Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
export const IAccessibilityService = createDecorator<IAccessibilityService>('accessibilityService');
export interface IAccessibilityService {
readonly _serviceBrand: undefined;
readonly onDidChangeScreenReaderOptimized: Event<void>;
alwaysUnderlineAccessKeys(): Promise<boolean>;
isScreenReaderOptimized(): boolean;
getAccessibilitySupport(): AccessibilitySupport;
setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void;
}
export const enum AccessibilitySupport {
/**
* This should be the browser case where it is not known if a screen reader is attached or no.
*/
Unknown = 0,
Disabled = 1,
Enabled = 2
}
export const CONTEXT_ACCESSIBILITY_MODE_ENABLED = new RawContextKey<boolean>('accessibilityModeEnabled', false);
export interface IAccessibilityInformation {
label: string;
role?: string;
}

View File

@@ -0,0 +1,61 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { IAccessibilityService, AccessibilitySupport, CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility';
import { Event, Emitter } from 'vs/base/common/event';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
export class AccessibilityService extends Disposable implements IAccessibilityService {
declare readonly _serviceBrand: undefined;
private _accessibilityModeEnabledContext: IContextKey<boolean>;
protected _accessibilitySupport = AccessibilitySupport.Unknown;
protected readonly _onDidChangeScreenReaderOptimized = new Emitter<void>();
constructor(
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IConfigurationService protected readonly _configurationService: IConfigurationService,
) {
super();
this._accessibilityModeEnabledContext = CONTEXT_ACCESSIBILITY_MODE_ENABLED.bindTo(this._contextKeyService);
const updateContextKey = () => this._accessibilityModeEnabledContext.set(this.isScreenReaderOptimized());
this._register(this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('editor.accessibilitySupport')) {
updateContextKey();
this._onDidChangeScreenReaderOptimized.fire();
}
}));
updateContextKey();
this.onDidChangeScreenReaderOptimized(() => updateContextKey());
}
get onDidChangeScreenReaderOptimized(): Event<void> {
return this._onDidChangeScreenReaderOptimized.event;
}
isScreenReaderOptimized(): boolean {
const config = this._configurationService.getValue('editor.accessibilitySupport');
return config === 'on' || (config === 'auto' && this._accessibilitySupport === AccessibilitySupport.Enabled);
}
getAccessibilitySupport(): AccessibilitySupport {
return this._accessibilitySupport;
}
alwaysUnderlineAccessKeys(): Promise<boolean> {
return Promise.resolve(false);
}
setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void {
if (this._accessibilitySupport === accessibilitySupport) {
return;
}
this._accessibilitySupport = accessibilitySupport;
this._onDidChangeScreenReaderOptimized.fire();
}
}

View File

@@ -0,0 +1,241 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createCSSRule, asCSSUrl, ModifierKeyEmitter } from 'vs/base/browser/dom';
import { domEvent } from 'vs/base/browser/event';
import { IAction, Separator } from 'vs/base/common/actions';
import { IdGenerator } from 'vs/base/common/idGenerator';
import { IDisposable, toDisposable, MutableDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { localize } from 'vs/nls';
import { ICommandAction, IMenu, IMenuActionOptions, MenuItemAction, SubmenuItemAction, Icon } from 'vs/platform/actions/common/actions';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem';
export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, isPrimaryGroup?: (group: string) => boolean): IDisposable {
const groups = menu.getActions(options);
const useAlternativeActions = ModifierKeyEmitter.getInstance().keyStatus.altKey;
fillInActions(groups, target, useAlternativeActions, isPrimaryGroup);
return asDisposable(groups);
}
export function createAndFillInActionBarActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, isPrimaryGroup?: (group: string) => boolean): IDisposable {
const groups = menu.getActions(options);
// Action bars handle alternative actions on their own so the alternative actions should be ignored
fillInActions(groups, target, false, isPrimaryGroup);
return asDisposable(groups);
}
function asDisposable(groups: ReadonlyArray<[string, ReadonlyArray<MenuItemAction | SubmenuItemAction>]>): IDisposable {
const disposables = new DisposableStore();
for (const [, actions] of groups) {
for (const action of actions) {
disposables.add(action);
}
}
return disposables;
}
function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray<MenuItemAction | SubmenuItemAction>]>, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, useAlternativeActions: boolean, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void {
for (let tuple of groups) {
let [group, actions] = tuple;
if (useAlternativeActions) {
actions = actions.map(a => (a instanceof MenuItemAction) && !!a.alt ? a.alt : a);
}
if (isPrimaryGroup(group)) {
const to = Array.isArray(target) ? target : target.primary;
to.unshift(...actions);
} else {
const to = Array.isArray(target) ? target : target.secondary;
if (to.length > 0) {
to.push(new Separator());
}
to.push(...actions);
}
}
}
const ids = new IdGenerator('menu-item-action-item-icon-');
const ICON_PATH_TO_CSS_RULES = new Map<string /* path*/, string /* CSS rule */>();
export class MenuEntryActionViewItem extends ActionViewItem {
private _wantsAltCommand: boolean = false;
private readonly _itemClassDispose = this._register(new MutableDisposable());
private readonly _altKey: ModifierKeyEmitter;
constructor(
readonly _action: MenuItemAction,
@IKeybindingService protected readonly _keybindingService: IKeybindingService,
@INotificationService protected _notificationService: INotificationService
) {
super(undefined, _action, { icon: !!(_action.class || _action.item.icon), label: !_action.class && !_action.item.icon });
this._altKey = ModifierKeyEmitter.getInstance();
}
protected get _commandAction(): IAction {
return this._wantsAltCommand && (<MenuItemAction>this._action).alt || this._action;
}
onClick(event: MouseEvent): void {
event.preventDefault();
event.stopPropagation();
this.actionRunner.run(this._commandAction, this._context)
.then(undefined, err => this._notificationService.error(err));
}
render(container: HTMLElement): void {
super.render(container);
this._updateItemClass(this._action.item);
let mouseOver = false;
let alternativeKeyDown = this._altKey.keyStatus.altKey;
const updateAltState = () => {
const wantsAltCommand = mouseOver && alternativeKeyDown;
if (wantsAltCommand !== this._wantsAltCommand) {
this._wantsAltCommand = wantsAltCommand;
this.updateLabel();
this.updateTooltip();
this.updateClass();
}
};
if (this._action.alt) {
this._register(this._altKey.event(value => {
alternativeKeyDown = value.altKey;
updateAltState();
}));
}
this._register(domEvent(container, 'mouseleave')(_ => {
mouseOver = false;
updateAltState();
}));
this._register(domEvent(container, 'mouseenter')(e => {
mouseOver = true;
updateAltState();
}));
}
updateLabel(): void {
if (this.options.label && this.label) {
this.label.textContent = this._commandAction.label;
}
}
updateTooltip(): void {
if (this.label) {
const keybinding = this._keybindingService.lookupKeybinding(this._commandAction.id);
const keybindingLabel = keybinding && keybinding.getLabel();
const tooltip = this._commandAction.tooltip || this._commandAction.label;
this.label.title = keybindingLabel
? localize('titleAndKb', "{0} ({1})", tooltip, keybindingLabel)
: tooltip;
}
}
updateClass(): void {
if (this.options.icon) {
if (this._commandAction !== this._action) {
if (this._action.alt) {
this._updateItemClass(this._action.alt.item);
}
} else if ((<MenuItemAction>this._action).alt) {
this._updateItemClass(this._action.item);
}
}
}
private _updateItemClass(item: ICommandAction): void {
this._itemClassDispose.value = undefined;
const icon = this._commandAction.checked && (item.toggled as { icon?: Icon })?.icon ? (item.toggled as { icon: Icon }).icon : item.icon;
if (ThemeIcon.isThemeIcon(icon)) {
// theme icons
const iconClass = ThemeIcon.asClassName(icon);
if (this.label && iconClass) {
this.label.classList.add(...iconClass.split(' '));
this._itemClassDispose.value = toDisposable(() => {
if (this.label) {
this.label.classList.remove(...iconClass.split(' '));
}
});
}
} else if (icon) {
// icon path
let iconClass: string;
if (icon.dark?.scheme) {
const iconPathMapKey = icon.dark.toString();
if (ICON_PATH_TO_CSS_RULES.has(iconPathMapKey)) {
iconClass = ICON_PATH_TO_CSS_RULES.get(iconPathMapKey)!;
} else {
iconClass = ids.nextId();
createCSSRule(`.icon.${iconClass}`, `background-image: ${asCSSUrl(icon.light || icon.dark)}`);
createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: ${asCSSUrl(icon.dark)}`);
ICON_PATH_TO_CSS_RULES.set(iconPathMapKey, iconClass);
}
if (this.label) {
this.label.classList.add('icon', ...iconClass.split(' '));
this._itemClassDispose.value = toDisposable(() => {
if (this.label) {
this.label.classList.remove('icon', ...iconClass.split(' '));
}
});
}
}
}
}
}
export class SubmenuEntryActionViewItem extends DropdownMenuActionViewItem {
constructor(
action: SubmenuItemAction,
@INotificationService _notificationService: INotificationService,
@IContextMenuService _contextMenuService: IContextMenuService
) {
let classNames: string | string[] | undefined;
if (action.item.icon) {
if (ThemeIcon.isThemeIcon(action.item.icon)) {
classNames = ThemeIcon.asClassName(action.item.icon)!;
} else if (action.item.icon.dark?.scheme) {
const iconPathMapKey = action.item.icon.dark.toString();
if (ICON_PATH_TO_CSS_RULES.has(iconPathMapKey)) {
classNames = ['icon', ICON_PATH_TO_CSS_RULES.get(iconPathMapKey)!];
} else {
const className = ids.nextId();
classNames = ['icon', className];
createCSSRule(`.icon.${className}`, `background-image: ${asCSSUrl(action.item.icon.light || action.item.icon.dark)}`);
createCSSRule(`.vs-dark .icon.${className}, .hc-black .icon.${className}`, `background-image: ${asCSSUrl(action.item.icon.dark)}`);
ICON_PATH_TO_CSS_RULES.set(iconPathMapKey, className);
}
}
}
super(action, action.actions, _contextMenuService, { classNames: classNames, menuAsChild: true });
}
}

View File

@@ -0,0 +1,535 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions';
import { SyncDescriptor0, createSyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { IConstructorSignature2, createDecorator, BrandedService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindings, KeybindingsRegistry, IKeybindingRule } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ContextKeyExpr, IContextKeyService, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
import { ICommandService, CommandsRegistry, ICommandHandlerDescription } from 'vs/platform/commands/common/commands';
import { IDisposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { Event, Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { UriDto } from 'vs/base/common/types';
import { Iterable } from 'vs/base/common/iterator';
import { LinkedList } from 'vs/base/common/linkedList';
export interface ILocalizedString {
value: string;
original: string;
}
export type Icon = { dark?: URI; light?: URI; } | ThemeIcon;
export interface ICommandAction {
id: string;
title: string | ILocalizedString;
category?: string | ILocalizedString;
tooltip?: string | ILocalizedString;
icon?: Icon;
precondition?: ContextKeyExpression;
toggled?: ContextKeyExpression | { condition: ContextKeyExpression, icon?: Icon, tooltip?: string | ILocalizedString };
}
export type ISerializableCommandAction = UriDto<ICommandAction>;
export interface IMenuItem {
command: ICommandAction;
alt?: ICommandAction;
when?: ContextKeyExpression;
group?: 'navigation' | string;
order?: number;
}
export interface ISubmenuItem {
title: string | ILocalizedString;
submenu: MenuId;
icon?: Icon;
when?: ContextKeyExpression;
group?: 'navigation' | string;
order?: number;
}
export function isIMenuItem(item: IMenuItem | ISubmenuItem): item is IMenuItem {
return (item as IMenuItem).command !== undefined;
}
export function isISubmenuItem(item: IMenuItem | ISubmenuItem): item is ISubmenuItem {
return (item as ISubmenuItem).submenu !== undefined;
}
export class MenuId {
private static _idPool = 0;
static readonly CommandPalette = new MenuId('CommandPalette');
static readonly DebugBreakpointsContext = new MenuId('DebugBreakpointsContext');
static readonly DebugCallStackContext = new MenuId('DebugCallStackContext');
static readonly DebugConsoleContext = new MenuId('DebugConsoleContext');
static readonly DebugVariablesContext = new MenuId('DebugVariablesContext');
static readonly DebugWatchContext = new MenuId('DebugWatchContext');
static readonly DebugToolBar = new MenuId('DebugToolBar');
static readonly EditorContext = new MenuId('EditorContext');
static readonly EditorContextPeek = new MenuId('EditorContextPeek');
static readonly EditorTitle = new MenuId('EditorTitle');
static readonly EditorTitleContext = new MenuId('EditorTitleContext');
static readonly EmptyEditorGroupContext = new MenuId('EmptyEditorGroupContext');
static readonly ExplorerContext = new MenuId('ExplorerContext');
static readonly ExtensionContext = new MenuId('ExtensionContext');
static readonly GlobalActivity = new MenuId('GlobalActivity');
static readonly MenubarAppearanceMenu = new MenuId('MenubarAppearanceMenu');
static readonly MenubarDebugMenu = new MenuId('MenubarDebugMenu');
static readonly MenubarEditMenu = new MenuId('MenubarEditMenu');
static readonly MenubarFileMenu = new MenuId('MenubarFileMenu');
static readonly MenubarGoMenu = new MenuId('MenubarGoMenu');
static readonly MenubarHelpMenu = new MenuId('MenubarHelpMenu');
static readonly MenubarLayoutMenu = new MenuId('MenubarLayoutMenu');
static readonly MenubarNewBreakpointMenu = new MenuId('MenubarNewBreakpointMenu');
static readonly MenubarPreferencesMenu = new MenuId('MenubarPreferencesMenu');
static readonly MenubarRecentMenu = new MenuId('MenubarRecentMenu');
static readonly MenubarSelectionMenu = new MenuId('MenubarSelectionMenu');
static readonly MenubarSwitchEditorMenu = new MenuId('MenubarSwitchEditorMenu');
static readonly MenubarSwitchGroupMenu = new MenuId('MenubarSwitchGroupMenu');
static readonly MenubarTerminalMenu = new MenuId('MenubarTerminalMenu');
static readonly MenubarViewMenu = new MenuId('MenubarViewMenu');
static readonly MenubarWebNavigationMenu = new MenuId('MenubarWebNavigationMenu');
static readonly OpenEditorsContext = new MenuId('OpenEditorsContext');
static readonly ProblemsPanelContext = new MenuId('ProblemsPanelContext');
static readonly SCMChangeContext = new MenuId('SCMChangeContext');
static readonly SCMResourceContext = new MenuId('SCMResourceContext');
static readonly SCMResourceFolderContext = new MenuId('SCMResourceFolderContext');
static readonly SCMResourceGroupContext = new MenuId('SCMResourceGroupContext');
static readonly SCMSourceControl = new MenuId('SCMSourceControl');
static readonly SCMTitle = new MenuId('SCMTitle');
static readonly SearchContext = new MenuId('SearchContext');
static readonly StatusBarWindowIndicatorMenu = new MenuId('StatusBarWindowIndicatorMenu');
static readonly TouchBarContext = new MenuId('TouchBarContext');
static readonly TitleBarContext = new MenuId('TitleBarContext');
static readonly TunnelContext = new MenuId('TunnelContext');
static readonly TunnelInline = new MenuId('TunnelInline');
static readonly TunnelTitle = new MenuId('TunnelTitle');
static readonly ViewItemContext = new MenuId('ViewItemContext');
static readonly ViewContainerTitleContext = new MenuId('ViewContainerTitleContext');
static readonly ViewTitle = new MenuId('ViewTitle');
static readonly ViewTitleContext = new MenuId('ViewTitleContext');
static readonly CommentThreadTitle = new MenuId('CommentThreadTitle');
static readonly CommentThreadActions = new MenuId('CommentThreadActions');
static readonly CommentTitle = new MenuId('CommentTitle');
static readonly CommentActions = new MenuId('CommentActions');
static readonly NotebookCellTitle = new MenuId('NotebookCellTitle');
static readonly NotebookCellInsert = new MenuId('NotebookCellInsert');
static readonly NotebookCellBetween = new MenuId('NotebookCellBetween');
static readonly NotebookCellListTop = new MenuId('NotebookCellTop');
static readonly NotebookDiffCellInputTitle = new MenuId('NotebookDiffCellInputTitle');
static readonly NotebookDiffCellMetadataTitle = new MenuId('NotebookDiffCellMetadataTitle');
static readonly NotebookDiffCellOutputsTitle = new MenuId('NotebookDiffCellOutputsTitle');
static readonly BulkEditTitle = new MenuId('BulkEditTitle');
static readonly BulkEditContext = new MenuId('BulkEditContext');
static readonly TimelineItemContext = new MenuId('TimelineItemContext');
static readonly TimelineTitle = new MenuId('TimelineTitle');
static readonly TimelineTitleContext = new MenuId('TimelineTitleContext');
static readonly AccountsContext = new MenuId('AccountsContext');
readonly id: number;
readonly _debugName: string;
constructor(debugName: string) {
this.id = MenuId._idPool++;
this._debugName = debugName;
}
}
export interface IMenuActionOptions {
arg?: any;
shouldForwardArgs?: boolean;
}
export interface IMenu extends IDisposable {
readonly onDidChange: Event<IMenu | undefined>;
getActions(options?: IMenuActionOptions): [string, Array<MenuItemAction | SubmenuItemAction>][];
}
export const IMenuService = createDecorator<IMenuService>('menuService');
export interface IMenuService {
readonly _serviceBrand: undefined;
createMenu(id: MenuId, scopedKeybindingService: IContextKeyService): IMenu;
}
export type ICommandsMap = Map<string, ICommandAction>;
export interface IMenuRegistryChangeEvent {
has(id: MenuId): boolean;
}
export interface IMenuRegistry {
readonly onDidChangeMenu: Event<IMenuRegistryChangeEvent>;
addCommands(newCommands: Iterable<ICommandAction>): IDisposable;
addCommand(userCommand: ICommandAction): IDisposable;
getCommand(id: string): ICommandAction | undefined;
getCommands(): ICommandsMap;
appendMenuItems(items: Iterable<{ id: MenuId, item: IMenuItem | ISubmenuItem }>): IDisposable;
appendMenuItem(menu: MenuId, item: IMenuItem | ISubmenuItem): IDisposable;
getMenuItems(loc: MenuId): Array<IMenuItem | ISubmenuItem>;
}
export const MenuRegistry: IMenuRegistry = new class implements IMenuRegistry {
private readonly _commands = new Map<string, ICommandAction>();
private readonly _menuItems = new Map<MenuId, LinkedList<IMenuItem | ISubmenuItem>>();
private readonly _onDidChangeMenu = new Emitter<IMenuRegistryChangeEvent>();
readonly onDidChangeMenu: Event<IMenuRegistryChangeEvent> = this._onDidChangeMenu.event;
addCommand(command: ICommandAction): IDisposable {
return this.addCommands(Iterable.single(command));
}
private readonly _commandPaletteChangeEvent: IMenuRegistryChangeEvent = {
has: id => id === MenuId.CommandPalette
};
addCommands(commands: Iterable<ICommandAction>): IDisposable {
for (const command of commands) {
this._commands.set(command.id, command);
}
this._onDidChangeMenu.fire(this._commandPaletteChangeEvent);
return toDisposable(() => {
let didChange = false;
for (const command of commands) {
didChange = this._commands.delete(command.id) || didChange;
}
if (didChange) {
this._onDidChangeMenu.fire(this._commandPaletteChangeEvent);
}
});
}
getCommand(id: string): ICommandAction | undefined {
return this._commands.get(id);
}
getCommands(): ICommandsMap {
const map = new Map<string, ICommandAction>();
this._commands.forEach((value, key) => map.set(key, value));
return map;
}
appendMenuItem(id: MenuId, item: IMenuItem | ISubmenuItem): IDisposable {
return this.appendMenuItems(Iterable.single({ id, item }));
}
appendMenuItems(items: Iterable<{ id: MenuId, item: IMenuItem | ISubmenuItem }>): IDisposable {
const changedIds = new Set<MenuId>();
const toRemove = new LinkedList<Function>();
for (const { id, item } of items) {
let list = this._menuItems.get(id);
if (!list) {
list = new LinkedList();
this._menuItems.set(id, list);
}
toRemove.push(list.push(item));
changedIds.add(id);
}
this._onDidChangeMenu.fire(changedIds);
return toDisposable(() => {
if (toRemove.size > 0) {
for (let fn of toRemove) {
fn();
}
this._onDidChangeMenu.fire(changedIds);
toRemove.clear();
}
});
}
getMenuItems(id: MenuId): Array<IMenuItem | ISubmenuItem> {
let result: Array<IMenuItem | ISubmenuItem>;
if (this._menuItems.has(id)) {
result = [...this._menuItems.get(id)!];
} else {
result = [];
}
if (id === MenuId.CommandPalette) {
// CommandPalette is special because it shows
// all commands by default
this._appendImplicitItems(result);
}
return result;
}
private _appendImplicitItems(result: Array<IMenuItem | ISubmenuItem>) {
const set = new Set<string>();
for (const item of result) {
if (isIMenuItem(item)) {
set.add(item.command.id);
if (item.alt) {
set.add(item.alt.id);
}
}
}
this._commands.forEach((command, id) => {
if (!set.has(id)) {
result.push({ command });
}
});
}
};
export class ExecuteCommandAction extends Action {
constructor(
id: string,
label: string,
@ICommandService private readonly _commandService: ICommandService) {
super(id, label);
}
run(...args: any[]): Promise<any> {
return this._commandService.executeCommand(this.id, ...args);
}
}
export class SubmenuItemAction extends SubmenuAction {
readonly item: ISubmenuItem;
constructor(
item: ISubmenuItem,
menuService: IMenuService,
contextKeyService: IContextKeyService,
options?: IMenuActionOptions
) {
const result: IAction[] = [];
const menu = menuService.createMenu(item.submenu, contextKeyService);
const groups = menu.getActions(options);
menu.dispose();
for (let group of groups) {
const [, actions] = group;
if (actions.length > 0) {
result.push(...actions);
result.push(new Separator());
}
}
if (result.length) {
result.pop(); // remove last separator
}
super(`submenuitem.${item.submenu.id}`, typeof item.title === 'string' ? item.title : item.title.value, result, 'submenu');
this.item = item;
}
}
export class MenuItemAction extends ExecuteCommandAction {
readonly item: ICommandAction;
readonly alt: MenuItemAction | undefined;
private _options: IMenuActionOptions;
constructor(
item: ICommandAction,
alt: ICommandAction | undefined,
options: IMenuActionOptions,
@IContextKeyService contextKeyService: IContextKeyService,
@ICommandService commandService: ICommandService
) {
typeof item.title === 'string' ? super(item.id, item.title, commandService) : super(item.id, item.title.value, commandService);
this._cssClass = undefined;
this._enabled = !item.precondition || contextKeyService.contextMatchesRules(item.precondition);
this._tooltip = item.tooltip ? typeof item.tooltip === 'string' ? item.tooltip : item.tooltip.value : undefined;
if (item.toggled) {
const toggled = ((item.toggled as { condition: ContextKeyExpression }).condition ? item.toggled : { condition: item.toggled }) as {
condition: ContextKeyExpression, icon?: Icon, tooltip?: string | ILocalizedString
};
this._checked = contextKeyService.contextMatchesRules(toggled.condition);
if (this._checked && toggled.tooltip) {
this._tooltip = typeof toggled.tooltip === 'string' ? toggled.tooltip : toggled.tooltip.value;
}
}
this._options = options || {};
this.item = item;
this.alt = alt ? new MenuItemAction(alt, undefined, this._options, contextKeyService, commandService) : undefined;
}
dispose(): void {
if (this.alt) {
this.alt.dispose();
}
super.dispose();
}
run(...args: any[]): Promise<any> {
let runArgs: any[] = [];
if (this._options.arg) {
runArgs = [...runArgs, this._options.arg];
}
if (this._options.shouldForwardArgs) {
runArgs = [...runArgs, ...args];
}
return super.run(...runArgs);
}
}
export class SyncActionDescriptor {
private readonly _descriptor: SyncDescriptor0<Action>;
private readonly _id: string;
private readonly _label?: string;
private readonly _keybindings: IKeybindings | undefined;
private readonly _keybindingContext: ContextKeyExpression | undefined;
private readonly _keybindingWeight: number | undefined;
public static create<Services extends BrandedService[]>(ctor: { new(id: string, label: string, ...services: Services): Action },
id: string, label: string | undefined, keybindings?: IKeybindings, keybindingContext?: ContextKeyExpression, keybindingWeight?: number
): SyncActionDescriptor {
return new SyncActionDescriptor(ctor as IConstructorSignature2<string, string | undefined, Action>, id, label, keybindings, keybindingContext, keybindingWeight);
}
public static from<Services extends BrandedService[]>(
ctor: {
new(id: string, label: string, ...services: Services): Action;
readonly ID: string;
readonly LABEL: string;
},
keybindings?: IKeybindings, keybindingContext?: ContextKeyExpression, keybindingWeight?: number
): SyncActionDescriptor {
return SyncActionDescriptor.create(ctor, ctor.ID, ctor.LABEL, keybindings, keybindingContext, keybindingWeight);
}
private constructor(ctor: IConstructorSignature2<string, string | undefined, Action>,
id: string, label: string | undefined, keybindings?: IKeybindings, keybindingContext?: ContextKeyExpression, keybindingWeight?: number
) {
this._id = id;
this._label = label;
this._keybindings = keybindings;
this._keybindingContext = keybindingContext;
this._keybindingWeight = keybindingWeight;
this._descriptor = createSyncDescriptor(ctor, this._id, this._label);
}
public get syncDescriptor(): SyncDescriptor0<Action> {
return this._descriptor;
}
public get id(): string {
return this._id;
}
public get label(): string | undefined {
return this._label;
}
public get keybindings(): IKeybindings | undefined {
return this._keybindings;
}
public get keybindingContext(): ContextKeyExpression | undefined {
return this._keybindingContext;
}
public get keybindingWeight(): number | undefined {
return this._keybindingWeight;
}
}
//#region --- IAction2
type OneOrN<T> = T | T[];
export interface IAction2Options extends ICommandAction {
/**
* Shorthand to add this command to the command palette
*/
f1?: boolean;
/**
* One or many menu items.
*/
menu?: OneOrN<{ id: MenuId } & Omit<IMenuItem, 'command'>>;
/**
* One keybinding.
*/
keybinding?: OneOrN<Omit<IKeybindingRule, 'id'>>;
/**
* Metadata about this command, used for API commands or when
* showing keybindings that have no other UX.
*/
description?: ICommandHandlerDescription;
}
export abstract class Action2 {
constructor(readonly desc: Readonly<IAction2Options>) { }
abstract run(accessor: ServicesAccessor, ...args: any[]): any;
}
export function registerAction2(ctor: { new(): Action2 }): IDisposable {
const disposables = new DisposableStore();
const action = new ctor();
const { f1, menu, keybinding, description, ...command } = action.desc;
// command
disposables.add(CommandsRegistry.registerCommand({
id: command.id,
handler: (accessor, ...args) => action.run(accessor, ...args),
description: description,
}));
// menu
if (Array.isArray(menu)) {
disposables.add(MenuRegistry.appendMenuItems(menu.map(item => ({ id: item.id, item: { command, ...item } }))));
} else if (menu) {
disposables.add(MenuRegistry.appendMenuItem(menu.id, { command, ...menu }));
}
if (f1) {
disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command, when: command.precondition }));
disposables.add(MenuRegistry.addCommand(command));
}
// keybinding
if (Array.isArray(keybinding)) {
for (let item of keybinding) {
KeybindingsRegistry.registerKeybindingRule({
...item,
id: command.id,
when: command.precondition ? ContextKeyExpr.and(command.precondition, item.when) : item.when
});
}
} else if (keybinding) {
KeybindingsRegistry.registerKeybindingRule({
...keybinding,
id: command.id,
when: command.precondition ? ContextKeyExpr.and(command.precondition, keybinding.when) : keybinding.when
});
}
return disposables;
}
//#endregion

View File

@@ -0,0 +1,187 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IMenu, IMenuActionOptions, IMenuItem, IMenuService, isIMenuItem, ISubmenuItem, MenuId, MenuItemAction, MenuRegistry, SubmenuItemAction, ILocalizedString } from 'vs/platform/actions/common/actions';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IContextKeyService, IContextKeyChangeEvent, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
export class MenuService implements IMenuService {
declare readonly _serviceBrand: undefined;
constructor(
@ICommandService private readonly _commandService: ICommandService
) {
//
}
createMenu(id: MenuId, contextKeyService: IContextKeyService): IMenu {
return new Menu(id, this._commandService, contextKeyService, this);
}
}
type MenuItemGroup = [string, Array<IMenuItem | ISubmenuItem>];
class Menu implements IMenu {
private readonly _onDidChange = new Emitter<IMenu | undefined>();
private readonly _dispoables = new DisposableStore();
private _menuGroups: MenuItemGroup[] = [];
private _contextKeys: Set<string> = new Set();
constructor(
private readonly _id: MenuId,
@ICommandService private readonly _commandService: ICommandService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IMenuService private readonly _menuService: IMenuService
) {
this._build();
// rebuild this menu whenever the menu registry reports an
// event for this MenuId
this._dispoables.add(Event.debounce(
Event.filter(MenuRegistry.onDidChangeMenu, set => set.has(this._id)),
() => { },
50
)(this._build, this));
// when context keys change we need to check if the menu also
// has changed
this._dispoables.add(Event.debounce<IContextKeyChangeEvent, boolean>(
this._contextKeyService.onDidChangeContext,
(last, event) => last || event.affectsSome(this._contextKeys),
50
)(e => e && this._onDidChange.fire(undefined), this));
}
dispose(): void {
this._dispoables.dispose();
this._onDidChange.dispose();
}
private _build(): void {
// reset
this._menuGroups.length = 0;
this._contextKeys.clear();
const menuItems = MenuRegistry.getMenuItems(this._id);
let group: MenuItemGroup | undefined;
menuItems.sort(Menu._compareMenuItems);
for (let item of menuItems) {
// group by groupId
const groupName = item.group || '';
if (!group || group[0] !== groupName) {
group = [groupName, []];
this._menuGroups.push(group);
}
group![1].push(item);
// keep keys for eventing
Menu._fillInKbExprKeys(item.when, this._contextKeys);
// keep precondition keys for event if applicable
if (isIMenuItem(item) && item.command.precondition) {
Menu._fillInKbExprKeys(item.command.precondition, this._contextKeys);
}
// keep toggled keys for event if applicable
if (isIMenuItem(item) && item.command.toggled) {
const toggledExpression: ContextKeyExpression = (item.command.toggled as { condition: ContextKeyExpression }).condition || item.command.toggled;
Menu._fillInKbExprKeys(toggledExpression, this._contextKeys);
}
}
this._onDidChange.fire(this);
}
get onDidChange(): Event<IMenu | undefined> {
return this._onDidChange.event;
}
getActions(options: IMenuActionOptions): [string, Array<MenuItemAction | SubmenuItemAction>][] {
const result: [string, Array<MenuItemAction | SubmenuItemAction>][] = [];
for (let group of this._menuGroups) {
const [id, items] = group;
const activeActions: Array<MenuItemAction | SubmenuItemAction> = [];
for (const item of items) {
if (this._contextKeyService.contextMatchesRules(item.when)) {
const action = isIMenuItem(item)
? new MenuItemAction(item.command, item.alt, options, this._contextKeyService, this._commandService)
: new SubmenuItemAction(item, this._menuService, this._contextKeyService, options);
activeActions.push(action);
}
}
if (activeActions.length > 0) {
result.push([id, activeActions]);
}
}
return result;
}
private static _fillInKbExprKeys(exp: ContextKeyExpression | undefined, set: Set<string>): void {
if (exp) {
for (let key of exp.keys()) {
set.add(key);
}
}
}
private static _compareMenuItems(a: IMenuItem | ISubmenuItem, b: IMenuItem | ISubmenuItem): number {
let aGroup = a.group;
let bGroup = b.group;
if (aGroup !== bGroup) {
// Falsy groups come last
if (!aGroup) {
return 1;
} else if (!bGroup) {
return -1;
}
// 'navigation' group comes first
if (aGroup === 'navigation') {
return -1;
} else if (bGroup === 'navigation') {
return 1;
}
// lexical sort for groups
let value = aGroup.localeCompare(bGroup);
if (value !== 0) {
return value;
}
}
// sort on priority - default is 0
let aPrio = a.order || 0;
let bPrio = b.order || 0;
if (aPrio < bPrio) {
return -1;
} else if (aPrio > bPrio) {
return 1;
}
// sort on titles
return Menu._compareTitles(
isIMenuItem(a) ? a.command.title : a.title,
isIMenuItem(b) ? b.command.title : b.title
);
}
private static _compareTitles(a: string | ILocalizedString, b: string | ILocalizedString) {
const aStr = typeof a === 'string' ? a : a.original;
const bStr = typeof b === 'string' ? b : b.original;
return aStr.localeCompare(bStr);
}
}

View File

@@ -0,0 +1,203 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { MenuRegistry, MenuId, isIMenuItem } from 'vs/platform/actions/common/actions';
import { MenuService } from 'vs/platform/actions/common/menuService';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { NullCommandService } from 'vs/platform/commands/common/commands';
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
// --- service instances
const contextKeyService = new class extends MockContextKeyService {
contextMatchesRules() {
return true;
}
};
// --- tests
suite('MenuService', function () {
let menuService: MenuService;
const disposables = new DisposableStore();
let testMenuId: MenuId;
setup(function () {
menuService = new MenuService(NullCommandService);
testMenuId = new MenuId('testo');
disposables.clear();
});
teardown(function () {
disposables.clear();
});
test('group sorting', function () {
disposables.add(MenuRegistry.appendMenuItem(testMenuId, {
command: { id: 'one', title: 'FOO' },
group: '0_hello'
}));
disposables.add(MenuRegistry.appendMenuItem(testMenuId, {
command: { id: 'two', title: 'FOO' },
group: 'hello'
}));
disposables.add(MenuRegistry.appendMenuItem(testMenuId, {
command: { id: 'three', title: 'FOO' },
group: 'Hello'
}));
disposables.add(MenuRegistry.appendMenuItem(testMenuId, {
command: { id: 'four', title: 'FOO' },
group: ''
}));
disposables.add(MenuRegistry.appendMenuItem(testMenuId, {
command: { id: 'five', title: 'FOO' },
group: 'navigation'
}));
const groups = menuService.createMenu(testMenuId, contextKeyService).getActions();
assert.equal(groups.length, 5);
const [one, two, three, four, five] = groups;
assert.equal(one[0], 'navigation');
assert.equal(two[0], '0_hello');
assert.equal(three[0], 'hello');
assert.equal(four[0], 'Hello');
assert.equal(five[0], '');
});
test('in group sorting, by title', function () {
disposables.add(MenuRegistry.appendMenuItem(testMenuId, {
command: { id: 'a', title: 'aaa' },
group: 'Hello'
}));
disposables.add(MenuRegistry.appendMenuItem(testMenuId, {
command: { id: 'b', title: 'fff' },
group: 'Hello'
}));
disposables.add(MenuRegistry.appendMenuItem(testMenuId, {
command: { id: 'c', title: 'zzz' },
group: 'Hello'
}));
const groups = menuService.createMenu(testMenuId, contextKeyService).getActions();
assert.equal(groups.length, 1);
const [, actions] = groups[0];
assert.equal(actions.length, 3);
const [one, two, three] = actions;
assert.equal(one.id, 'a');
assert.equal(two.id, 'b');
assert.equal(three.id, 'c');
});
test('in group sorting, by title and order', function () {
disposables.add(MenuRegistry.appendMenuItem(testMenuId, {
command: { id: 'a', title: 'aaa' },
group: 'Hello',
order: 10
}));
disposables.add(MenuRegistry.appendMenuItem(testMenuId, {
command: { id: 'b', title: 'fff' },
group: 'Hello'
}));
disposables.add(MenuRegistry.appendMenuItem(testMenuId, {
command: { id: 'c', title: 'zzz' },
group: 'Hello',
order: -1
}));
disposables.add(MenuRegistry.appendMenuItem(testMenuId, {
command: { id: 'd', title: 'yyy' },
group: 'Hello',
order: -1
}));
const groups = menuService.createMenu(testMenuId, contextKeyService).getActions();
assert.equal(groups.length, 1);
const [, actions] = groups[0];
assert.equal(actions.length, 4);
const [one, two, three, four] = actions;
assert.equal(one.id, 'd');
assert.equal(two.id, 'c');
assert.equal(three.id, 'b');
assert.equal(four.id, 'a');
});
test('in group sorting, special: navigation', function () {
disposables.add(MenuRegistry.appendMenuItem(testMenuId, {
command: { id: 'a', title: 'aaa' },
group: 'navigation',
order: 1.3
}));
disposables.add(MenuRegistry.appendMenuItem(testMenuId, {
command: { id: 'b', title: 'fff' },
group: 'navigation',
order: 1.2
}));
disposables.add(MenuRegistry.appendMenuItem(testMenuId, {
command: { id: 'c', title: 'zzz' },
group: 'navigation',
order: 1.1
}));
const groups = menuService.createMenu(testMenuId, contextKeyService).getActions();
assert.equal(groups.length, 1);
const [[, actions]] = groups;
assert.equal(actions.length, 3);
const [one, two, three] = actions;
assert.equal(one.id, 'c');
assert.equal(two.id, 'b');
assert.equal(three.id, 'a');
});
test('special MenuId palette', function () {
disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: { id: 'a', title: 'Explicit' }
}));
MenuRegistry.addCommand({ id: 'b', title: 'Implicit' });
let foundA = false;
let foundB = false;
for (const item of MenuRegistry.getMenuItems(MenuId.CommandPalette)) {
if (isIMenuItem(item)) {
if (item.command.id === 'a') {
assert.equal(item.command.title, 'Explicit');
foundA = true;
}
if (item.command.id === 'b') {
assert.equal(item.command.title, 'Implicit');
foundB = true;
}
}
}
assert.equal(foundA, true);
assert.equal(foundB, true);
});
});

View File

@@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { URI } from 'vs/base/common/uri';
import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
export const IBackupMainService = createDecorator<IBackupMainService>('backupMainService');
export interface IWorkspaceBackupInfo {
workspace: IWorkspaceIdentifier;
remoteAuthority?: string;
}
export function isWorkspaceBackupInfo(obj: unknown): obj is IWorkspaceBackupInfo {
const candidate = obj as IWorkspaceBackupInfo;
return candidate && isWorkspaceIdentifier(candidate.workspace);
}
export interface IBackupMainService {
readonly _serviceBrand: undefined;
isHotExitEnabled(): boolean;
getWorkspaceBackups(): IWorkspaceBackupInfo[];
getFolderBackupPaths(): URI[];
getEmptyWindowBackupPaths(): IEmptyWindowBackupInfo[];
registerWorkspaceBackupSync(workspace: IWorkspaceBackupInfo, migrateFrom?: string): string;
registerFolderBackupSync(folderUri: URI): string;
registerEmptyWindowBackupSync(backupFolder?: string, remoteAuthority?: string): string;
unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void;
unregisterFolderBackupSync(folderUri: URI): void;
unregisterEmptyWindowBackupSync(backupFolder: string): void;
/**
* All folders or workspaces that are known to have
* backups stored. This call is long running because
* it checks for each backup location if any backups
* are stored.
*/
getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>>;
}

View File

@@ -0,0 +1,503 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as crypto from 'crypto';
import * as path from 'vs/base/common/path';
import * as platform from 'vs/base/common/platform';
import { writeFileSync, writeFile, readFile, readdir, exists, rimraf, rename, RimRafMode } from 'vs/base/node/pfs';
import { IBackupMainService, IWorkspaceBackupInfo, isWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup';
import { IBackupWorkspacesFormat, IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IFilesConfiguration, HotExitConfiguration } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { IWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { URI } from 'vs/base/common/uri';
import { isEqual } from 'vs/base/common/extpath';
import { Schemas } from 'vs/base/common/network';
import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources';
export class BackupMainService implements IBackupMainService {
declare readonly _serviceBrand: undefined;
protected backupHome: string;
protected workspacesJsonPath: string;
private workspaces: IWorkspaceBackupInfo[] = [];
private folders: URI[] = [];
private emptyWindows: IEmptyWindowBackupInfo[] = [];
// Comparers for paths and resources that will
// - ignore path casing on Windows/macOS
// - respect path casing on Linux
private readonly backupUriComparer = extUriBiasedIgnorePathCase;
private readonly backupPathComparer = { isEqual: (pathA: string, pathB: string) => isEqual(pathA, pathB, !platform.isLinux) };
constructor(
@IEnvironmentMainService environmentService: IEnvironmentMainService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@ILogService private readonly logService: ILogService
) {
this.backupHome = environmentService.backupHome;
this.workspacesJsonPath = environmentService.backupWorkspacesPath;
}
async initialize(): Promise<void> {
let backups: IBackupWorkspacesFormat;
try {
backups = JSON.parse(await readFile(this.workspacesJsonPath, 'utf8')); // invalid JSON or permission issue can happen here
} catch (error) {
backups = Object.create(null);
}
// read empty workspaces backups first
if (backups.emptyWorkspaceInfos) {
this.emptyWindows = await this.validateEmptyWorkspaces(backups.emptyWorkspaceInfos);
} else if (Array.isArray(backups.emptyWorkspaces)) {
// read legacy entries
this.emptyWindows = await this.validateEmptyWorkspaces(backups.emptyWorkspaces.map(emptyWindow => ({ backupFolder: emptyWindow })));
}
// read workspace backups
let rootWorkspaces: IWorkspaceBackupInfo[] = [];
try {
if (Array.isArray(backups.rootURIWorkspaces)) {
rootWorkspaces = backups.rootURIWorkspaces.map(workspace => ({ workspace: { id: workspace.id, configPath: URI.parse(workspace.configURIPath) }, remoteAuthority: workspace.remoteAuthority }));
} else if (Array.isArray(backups.rootWorkspaces)) {
rootWorkspaces = backups.rootWorkspaces.map(workspace => ({ workspace: { id: workspace.id, configPath: URI.file(workspace.configPath) } }));
}
} catch (e) {
// ignore URI parsing exceptions
}
this.workspaces = await this.validateWorkspaces(rootWorkspaces);
// read folder backups
let workspaceFolders: URI[] = [];
try {
if (Array.isArray(backups.folderURIWorkspaces)) {
workspaceFolders = backups.folderURIWorkspaces.map(folder => URI.parse(folder));
} else if (Array.isArray(backups.folderWorkspaces)) {
// migrate legacy folder paths
workspaceFolders = [];
for (const folderPath of backups.folderWorkspaces) {
const oldFolderHash = this.getLegacyFolderHash(folderPath);
const folderUri = URI.file(folderPath);
const newFolderHash = this.getFolderHash(folderUri);
if (newFolderHash !== oldFolderHash) {
await this.moveBackupFolder(this.getBackupPath(newFolderHash), this.getBackupPath(oldFolderHash));
}
workspaceFolders.push(folderUri);
}
}
} catch (e) {
// ignore URI parsing exceptions
}
this.folders = await this.validateFolders(workspaceFolders);
// save again in case some workspaces or folders have been removed
await this.save();
}
getWorkspaceBackups(): IWorkspaceBackupInfo[] {
if (this.isHotExitOnExitAndWindowClose()) {
// Only non-folder windows are restored on main process launch when
// hot exit is configured as onExitAndWindowClose.
return [];
}
return this.workspaces.slice(0); // return a copy
}
getFolderBackupPaths(): URI[] {
if (this.isHotExitOnExitAndWindowClose()) {
// Only non-folder windows are restored on main process launch when
// hot exit is configured as onExitAndWindowClose.
return [];
}
return this.folders.slice(0); // return a copy
}
isHotExitEnabled(): boolean {
return this.getHotExitConfig() !== HotExitConfiguration.OFF;
}
private isHotExitOnExitAndWindowClose(): boolean {
return this.getHotExitConfig() === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE;
}
private getHotExitConfig(): string {
const config = this.configurationService.getValue<IFilesConfiguration>();
return config?.files?.hotExit || HotExitConfiguration.ON_EXIT;
}
getEmptyWindowBackupPaths(): IEmptyWindowBackupInfo[] {
return this.emptyWindows.slice(0); // return a copy
}
registerWorkspaceBackupSync(workspaceInfo: IWorkspaceBackupInfo, migrateFrom?: string): string {
if (!this.workspaces.some(workspace => workspaceInfo.workspace.id === workspace.workspace.id)) {
this.workspaces.push(workspaceInfo);
this.saveSync();
}
const backupPath = this.getBackupPath(workspaceInfo.workspace.id);
if (migrateFrom) {
this.moveBackupFolderSync(backupPath, migrateFrom);
}
return backupPath;
}
private moveBackupFolderSync(backupPath: string, moveFromPath: string): void {
// Target exists: make sure to convert existing backups to empty window backups
if (fs.existsSync(backupPath)) {
this.convertToEmptyWindowBackupSync(backupPath);
}
// When we have data to migrate from, move it over to the target location
if (fs.existsSync(moveFromPath)) {
try {
fs.renameSync(moveFromPath, backupPath);
} catch (ex) {
this.logService.error(`Backup: Could not move backup folder to new location: ${ex.toString()}`);
}
}
}
private async moveBackupFolder(backupPath: string, moveFromPath: string): Promise<void> {
// Target exists: make sure to convert existing backups to empty window backups
if (await exists(backupPath)) {
await this.convertToEmptyWindowBackup(backupPath);
}
// When we have data to migrate from, move it over to the target location
if (await exists(moveFromPath)) {
try {
await rename(moveFromPath, backupPath);
} catch (ex) {
this.logService.error(`Backup: Could not move backup folder to new location: ${ex.toString()}`);
}
}
}
unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void {
const id = workspace.id;
const index = this.workspaces.findIndex(workspace => workspace.workspace.id === id);
if (index !== -1) {
this.workspaces.splice(index, 1);
this.saveSync();
}
}
registerFolderBackupSync(folderUri: URI): string {
if (!this.folders.some(folder => this.backupUriComparer.isEqual(folderUri, folder))) {
this.folders.push(folderUri);
this.saveSync();
}
return this.getBackupPath(this.getFolderHash(folderUri));
}
unregisterFolderBackupSync(folderUri: URI): void {
const index = this.folders.findIndex(folder => this.backupUriComparer.isEqual(folderUri, folder));
if (index !== -1) {
this.folders.splice(index, 1);
this.saveSync();
}
}
registerEmptyWindowBackupSync(backupFolderCandidate?: string, remoteAuthority?: string): string {
// Generate a new folder if this is a new empty workspace
const backupFolder = backupFolderCandidate || this.getRandomEmptyWindowId();
if (!this.emptyWindows.some(emptyWindow => !!emptyWindow.backupFolder && this.backupPathComparer.isEqual(emptyWindow.backupFolder, backupFolder))) {
this.emptyWindows.push({ backupFolder, remoteAuthority });
this.saveSync();
}
return this.getBackupPath(backupFolder);
}
unregisterEmptyWindowBackupSync(backupFolder: string): void {
const index = this.emptyWindows.findIndex(emptyWindow => !!emptyWindow.backupFolder && this.backupPathComparer.isEqual(emptyWindow.backupFolder, backupFolder));
if (index !== -1) {
this.emptyWindows.splice(index, 1);
this.saveSync();
}
}
private getBackupPath(oldFolderHash: string): string {
return path.join(this.backupHome, oldFolderHash);
}
private async validateWorkspaces(rootWorkspaces: IWorkspaceBackupInfo[]): Promise<IWorkspaceBackupInfo[]> {
if (!Array.isArray(rootWorkspaces)) {
return [];
}
const seenIds: Set<string> = new Set();
const result: IWorkspaceBackupInfo[] = [];
// Validate Workspaces
for (let workspaceInfo of rootWorkspaces) {
const workspace = workspaceInfo.workspace;
if (!isWorkspaceIdentifier(workspace)) {
return []; // wrong format, skip all entries
}
if (!seenIds.has(workspace.id)) {
seenIds.add(workspace.id);
const backupPath = this.getBackupPath(workspace.id);
const hasBackups = await this.doHasBackups(backupPath);
// If the workspace has no backups, ignore it
if (hasBackups) {
if (workspace.configPath.scheme !== Schemas.file || await exists(workspace.configPath.fsPath)) {
result.push(workspaceInfo);
} else {
// If the workspace has backups, but the target workspace is missing, convert backups to empty ones
await this.convertToEmptyWindowBackup(backupPath);
}
} else {
await this.deleteStaleBackup(backupPath);
}
}
}
return result;
}
private async validateFolders(folderWorkspaces: URI[]): Promise<URI[]> {
if (!Array.isArray(folderWorkspaces)) {
return [];
}
const result: URI[] = [];
const seenIds: Set<string> = new Set();
for (let folderURI of folderWorkspaces) {
const key = this.backupUriComparer.getComparisonKey(folderURI);
if (!seenIds.has(key)) {
seenIds.add(key);
const backupPath = this.getBackupPath(this.getFolderHash(folderURI));
const hasBackups = await this.doHasBackups(backupPath);
// If the folder has no backups, ignore it
if (hasBackups) {
if (folderURI.scheme !== Schemas.file || await exists(folderURI.fsPath)) {
result.push(folderURI);
} else {
// If the folder has backups, but the target workspace is missing, convert backups to empty ones
await this.convertToEmptyWindowBackup(backupPath);
}
} else {
await this.deleteStaleBackup(backupPath);
}
}
}
return result;
}
private async validateEmptyWorkspaces(emptyWorkspaces: IEmptyWindowBackupInfo[]): Promise<IEmptyWindowBackupInfo[]> {
if (!Array.isArray(emptyWorkspaces)) {
return [];
}
const result: IEmptyWindowBackupInfo[] = [];
const seenIds: Set<string> = new Set();
// Validate Empty Windows
for (let backupInfo of emptyWorkspaces) {
const backupFolder = backupInfo.backupFolder;
if (typeof backupFolder !== 'string') {
return [];
}
if (!seenIds.has(backupFolder)) {
seenIds.add(backupFolder);
const backupPath = this.getBackupPath(backupFolder);
if (await this.doHasBackups(backupPath)) {
result.push(backupInfo);
} else {
await this.deleteStaleBackup(backupPath);
}
}
}
return result;
}
private async deleteStaleBackup(backupPath: string): Promise<void> {
try {
if (await exists(backupPath)) {
await rimraf(backupPath, RimRafMode.MOVE);
}
} catch (ex) {
this.logService.error(`Backup: Could not delete stale backup: ${ex.toString()}`);
}
}
private async convertToEmptyWindowBackup(backupPath: string): Promise<boolean> {
// New empty window backup
let newBackupFolder = this.getRandomEmptyWindowId();
while (this.emptyWindows.some(emptyWindow => !!emptyWindow.backupFolder && this.backupPathComparer.isEqual(emptyWindow.backupFolder, newBackupFolder))) {
newBackupFolder = this.getRandomEmptyWindowId();
}
// Rename backupPath to new empty window backup path
const newEmptyWindowBackupPath = this.getBackupPath(newBackupFolder);
try {
await rename(backupPath, newEmptyWindowBackupPath);
} catch (ex) {
this.logService.error(`Backup: Could not rename backup folder: ${ex.toString()}`);
return false;
}
this.emptyWindows.push({ backupFolder: newBackupFolder });
return true;
}
private convertToEmptyWindowBackupSync(backupPath: string): boolean {
// New empty window backup
let newBackupFolder = this.getRandomEmptyWindowId();
while (this.emptyWindows.some(emptyWindow => !!emptyWindow.backupFolder && this.backupPathComparer.isEqual(emptyWindow.backupFolder, newBackupFolder))) {
newBackupFolder = this.getRandomEmptyWindowId();
}
// Rename backupPath to new empty window backup path
const newEmptyWindowBackupPath = this.getBackupPath(newBackupFolder);
try {
fs.renameSync(backupPath, newEmptyWindowBackupPath);
} catch (ex) {
this.logService.error(`Backup: Could not rename backup folder: ${ex.toString()}`);
return false;
}
this.emptyWindows.push({ backupFolder: newBackupFolder });
return true;
}
async getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>> {
const dirtyWorkspaces: Array<IWorkspaceIdentifier | URI> = [];
// Workspaces with backups
for (const workspace of this.workspaces) {
if ((await this.hasBackups(workspace))) {
dirtyWorkspaces.push(workspace.workspace);
}
}
// Folders with backups
for (const folder of this.folders) {
if ((await this.hasBackups(folder))) {
dirtyWorkspaces.push(folder);
}
}
return dirtyWorkspaces;
}
private hasBackups(backupLocation: IWorkspaceBackupInfo | IEmptyWindowBackupInfo | URI): Promise<boolean> {
let backupPath: string;
// Folder
if (URI.isUri(backupLocation)) {
backupPath = this.getBackupPath(this.getFolderHash(backupLocation));
}
// Workspace
else if (isWorkspaceBackupInfo(backupLocation)) {
backupPath = this.getBackupPath(backupLocation.workspace.id);
}
// Empty
else {
backupPath = backupLocation.backupFolder;
}
return this.doHasBackups(backupPath);
}
private async doHasBackups(backupPath: string): Promise<boolean> {
try {
const backupSchemas = await readdir(backupPath);
for (const backupSchema of backupSchemas) {
try {
const backupSchemaChildren = await readdir(path.join(backupPath, backupSchema));
if (backupSchemaChildren.length > 0) {
return true;
}
} catch (error) {
// invalid folder
}
}
} catch (error) {
// backup path does not exist
}
return false;
}
private saveSync(): void {
try {
writeFileSync(this.workspacesJsonPath, JSON.stringify(this.serializeBackups()));
} catch (ex) {
this.logService.error(`Backup: Could not save workspaces.json: ${ex.toString()}`);
}
}
private async save(): Promise<void> {
try {
await writeFile(this.workspacesJsonPath, JSON.stringify(this.serializeBackups()));
} catch (ex) {
this.logService.error(`Backup: Could not save workspaces.json: ${ex.toString()}`);
}
}
private serializeBackups(): IBackupWorkspacesFormat {
return {
rootURIWorkspaces: this.workspaces.map(workspace => ({ id: workspace.workspace.id, configURIPath: workspace.workspace.configPath.toString(), remoteAuthority: workspace.remoteAuthority })),
folderURIWorkspaces: this.folders.map(folder => folder.toString()),
emptyWorkspaceInfos: this.emptyWindows,
emptyWorkspaces: this.emptyWindows.map(emptyWindow => emptyWindow.backupFolder)
};
}
private getRandomEmptyWindowId(): string {
return (Date.now() + Math.round(Math.random() * 1000)).toString();
}
protected getFolderHash(folderUri: URI): string {
let key: string;
if (folderUri.scheme === Schemas.file) {
// for backward compatibility, use the fspath as key
key = platform.isLinux ? folderUri.fsPath : folderUri.fsPath.toLowerCase();
} else {
key = folderUri.toString().toLowerCase();
}
return crypto.createHash('md5').update(key).digest('hex');
}
protected getLegacyFolderHash(folderPath: string): string {
return crypto.createHash('md5').update(platform.isLinux ? folderPath : folderPath.toLowerCase()).digest('hex');
}
}

View File

@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface ISerializedWorkspace { id: string; configURIPath: string; remoteAuthority?: string; }
export interface IBackupWorkspacesFormat {
rootURIWorkspaces: ISerializedWorkspace[];
folderURIWorkspaces: string[];
emptyWorkspaceInfos: IEmptyWindowBackupInfo[];
// deprecated
folderWorkspaces?: string[]; // use folderURIWorkspaces instead
emptyWorkspaces?: string[];
rootWorkspaces?: { id: string, configPath: string }[]; // use rootURIWorkspaces instead
}
export interface IEmptyWindowBackupInfo {
backupFolder: string;
remoteAuthority?: string;
}

View File

@@ -0,0 +1,776 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as platform from 'vs/base/common/platform';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'vs/base/common/path';
import * as pfs from 'vs/base/node/pfs';
import { URI } from 'vs/base/common/uri';
import { EnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainService';
import { IWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup';
import { IBackupWorkspacesFormat, ISerializedWorkspace } from 'vs/platform/backup/node/backup';
import { HotExitConfiguration } from 'vs/platform/files/common/files';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { ConsoleLogMainService } from 'vs/platform/log/common/log';
import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { createHash } from 'crypto';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { Schemas } from 'vs/base/common/network';
import { isEqual } from 'vs/base/common/resources';
suite('BackupMainService', () => {
function assertEqualUris(actual: URI[], expected: URI[]) {
assert.deepEqual(actual.map(a => a.toString()), expected.map(a => a.toString()));
}
const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupservice');
const backupHome = path.join(parentDir, 'Backups');
const backupWorkspacesPath = path.join(backupHome, 'workspaces.json');
const environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS));
class TestBackupMainService extends BackupMainService {
constructor(backupHome: string, backupWorkspacesPath: string, configService: TestConfigurationService) {
super(environmentService, configService, new ConsoleLogMainService());
this.backupHome = backupHome;
this.workspacesJsonPath = backupWorkspacesPath;
}
toBackupPath(arg: URI | string): string {
const id = arg instanceof URI ? super.getFolderHash(arg) : arg;
return path.join(this.backupHome, id);
}
getFolderHash(folderUri: URI): string {
return super.getFolderHash(folderUri);
}
toLegacyBackupPath(folderPath: string): string {
return path.join(this.backupHome, super.getLegacyFolderHash(folderPath));
}
}
function toWorkspace(path: string): IWorkspaceIdentifier {
return {
id: createHash('md5').update(sanitizePath(path)).digest('hex'),
configPath: URI.file(path)
};
}
function toWorkspaceBackupInfo(path: string, remoteAuthority?: string): IWorkspaceBackupInfo {
return {
workspace: {
id: createHash('md5').update(sanitizePath(path)).digest('hex'),
configPath: URI.file(path)
},
remoteAuthority
};
}
function toSerializedWorkspace(ws: IWorkspaceIdentifier): ISerializedWorkspace {
return {
id: ws.id,
configURIPath: ws.configPath.toString()
};
}
async function ensureFolderExists(uri: URI): Promise<void> {
if (!fs.existsSync(uri.fsPath)) {
fs.mkdirSync(uri.fsPath);
}
const backupFolder = service.toBackupPath(uri);
await createBackupFolder(backupFolder);
}
async function ensureWorkspaceExists(workspace: IWorkspaceIdentifier): Promise<IWorkspaceIdentifier> {
if (!fs.existsSync(workspace.configPath.fsPath)) {
await pfs.writeFile(workspace.configPath.fsPath, 'Hello');
}
const backupFolder = service.toBackupPath(workspace.id);
await createBackupFolder(backupFolder);
return workspace;
}
async function createBackupFolder(backupFolder: string): Promise<void> {
if (!fs.existsSync(backupFolder)) {
fs.mkdirSync(backupFolder);
fs.mkdirSync(path.join(backupFolder, Schemas.file));
await pfs.writeFile(path.join(backupFolder, Schemas.file, 'foo.txt'), 'Hello');
}
}
function sanitizePath(p: string): string {
return platform.isLinux ? p : p.toLowerCase();
}
const fooFile = URI.file(platform.isWindows ? 'C:\\foo' : '/foo');
const barFile = URI.file(platform.isWindows ? 'C:\\bar' : '/bar');
const existingTestFolder1 = URI.file(path.join(parentDir, 'folder1'));
let service: TestBackupMainService;
let configService: TestConfigurationService;
setup(async () => {
// Delete any existing backups completely and then re-create it.
await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);
await pfs.mkdirp(backupHome);
configService = new TestConfigurationService();
service = new TestBackupMainService(backupHome, backupWorkspacesPath, configService);
return service.initialize();
});
teardown(() => {
return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);
});
test('service validates backup workspaces on startup and cleans up (folder workspaces)', async function () {
this.timeout(1000 * 10); // increase timeout for this test
// 1) backup workspace path does not exist
service.registerFolderBackupSync(fooFile);
service.registerFolderBackupSync(barFile);
await service.initialize();
assertEqualUris(service.getFolderBackupPaths(), []);
// 2) backup workspace path exists with empty contents within
fs.mkdirSync(service.toBackupPath(fooFile));
fs.mkdirSync(service.toBackupPath(barFile));
service.registerFolderBackupSync(fooFile);
service.registerFolderBackupSync(barFile);
await service.initialize();
assertEqualUris(service.getFolderBackupPaths(), []);
assert.ok(!fs.existsSync(service.toBackupPath(fooFile)));
assert.ok(!fs.existsSync(service.toBackupPath(barFile)));
// 3) backup workspace path exists with empty folders within
fs.mkdirSync(service.toBackupPath(fooFile));
fs.mkdirSync(service.toBackupPath(barFile));
fs.mkdirSync(path.join(service.toBackupPath(fooFile), Schemas.file));
fs.mkdirSync(path.join(service.toBackupPath(barFile), Schemas.untitled));
service.registerFolderBackupSync(fooFile);
service.registerFolderBackupSync(barFile);
await service.initialize();
assertEqualUris(service.getFolderBackupPaths(), []);
assert.ok(!fs.existsSync(service.toBackupPath(fooFile)));
assert.ok(!fs.existsSync(service.toBackupPath(barFile)));
// 4) backup workspace path points to a workspace that no longer exists
// so it should convert the backup worspace to an empty workspace backup
const fileBackups = path.join(service.toBackupPath(fooFile), Schemas.file);
fs.mkdirSync(service.toBackupPath(fooFile));
fs.mkdirSync(service.toBackupPath(barFile));
fs.mkdirSync(fileBackups);
service.registerFolderBackupSync(fooFile);
assert.equal(service.getFolderBackupPaths().length, 1);
assert.equal(service.getEmptyWindowBackupPaths().length, 0);
fs.writeFileSync(path.join(fileBackups, 'backup.txt'), '');
await service.initialize();
assert.equal(service.getFolderBackupPaths().length, 0);
assert.equal(service.getEmptyWindowBackupPaths().length, 1);
});
test('service validates backup workspaces on startup and cleans up (root workspaces)', async function () {
this.timeout(1000 * 10); // increase timeout for this test
// 1) backup workspace path does not exist
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath));
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(barFile.fsPath));
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
// 2) backup workspace path exists with empty contents within
fs.mkdirSync(service.toBackupPath(fooFile));
fs.mkdirSync(service.toBackupPath(barFile));
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath));
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(barFile.fsPath));
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.ok(!fs.existsSync(service.toBackupPath(fooFile)));
assert.ok(!fs.existsSync(service.toBackupPath(barFile)));
// 3) backup workspace path exists with empty folders within
fs.mkdirSync(service.toBackupPath(fooFile));
fs.mkdirSync(service.toBackupPath(barFile));
fs.mkdirSync(path.join(service.toBackupPath(fooFile), Schemas.file));
fs.mkdirSync(path.join(service.toBackupPath(barFile), Schemas.untitled));
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath));
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(barFile.fsPath));
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.ok(!fs.existsSync(service.toBackupPath(fooFile)));
assert.ok(!fs.existsSync(service.toBackupPath(barFile)));
// 4) backup workspace path points to a workspace that no longer exists
// so it should convert the backup worspace to an empty workspace backup
const fileBackups = path.join(service.toBackupPath(fooFile), Schemas.file);
fs.mkdirSync(service.toBackupPath(fooFile));
fs.mkdirSync(service.toBackupPath(barFile));
fs.mkdirSync(fileBackups);
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath));
assert.equal(service.getWorkspaceBackups().length, 1);
assert.equal(service.getEmptyWindowBackupPaths().length, 0);
fs.writeFileSync(path.join(fileBackups, 'backup.txt'), '');
await service.initialize();
assert.equal(service.getWorkspaceBackups().length, 0);
assert.equal(service.getEmptyWindowBackupPaths().length, 1);
});
test('service supports to migrate backup data from another location', () => {
const backupPathToMigrate = service.toBackupPath(fooFile);
fs.mkdirSync(backupPathToMigrate);
fs.writeFileSync(path.join(backupPathToMigrate, 'backup.txt'), 'Some Data');
service.registerFolderBackupSync(URI.file(backupPathToMigrate));
const workspaceBackupPath = service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(barFile.fsPath), backupPathToMigrate);
assert.ok(fs.existsSync(workspaceBackupPath));
assert.ok(fs.existsSync(path.join(workspaceBackupPath, 'backup.txt')));
assert.ok(!fs.existsSync(backupPathToMigrate));
const emptyBackups = service.getEmptyWindowBackupPaths();
assert.equal(0, emptyBackups.length);
});
test('service backup migration makes sure to preserve existing backups', () => {
const backupPathToMigrate = service.toBackupPath(fooFile);
fs.mkdirSync(backupPathToMigrate);
fs.writeFileSync(path.join(backupPathToMigrate, 'backup.txt'), 'Some Data');
service.registerFolderBackupSync(URI.file(backupPathToMigrate));
const backupPathToPreserve = service.toBackupPath(barFile);
fs.mkdirSync(backupPathToPreserve);
fs.writeFileSync(path.join(backupPathToPreserve, 'backup.txt'), 'Some Data');
service.registerFolderBackupSync(URI.file(backupPathToPreserve));
const workspaceBackupPath = service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(barFile.fsPath), backupPathToMigrate);
assert.ok(fs.existsSync(workspaceBackupPath));
assert.ok(fs.existsSync(path.join(workspaceBackupPath, 'backup.txt')));
assert.ok(!fs.existsSync(backupPathToMigrate));
const emptyBackups = service.getEmptyWindowBackupPaths();
assert.equal(1, emptyBackups.length);
assert.equal(1, fs.readdirSync(path.join(backupHome, emptyBackups[0].backupFolder!)).length);
});
suite('migrate path to URI', () => {
test('migration folder path to URI makes sure to preserve existing backups', async () => {
let path1 = path.join(parentDir, 'folder1');
let path2 = path.join(parentDir, 'FOLDER2');
let uri1 = URI.file(path1);
let uri2 = URI.file(path2);
if (!fs.existsSync(path1)) {
fs.mkdirSync(path1);
}
if (!fs.existsSync(path2)) {
fs.mkdirSync(path2);
}
const backupFolder1 = service.toLegacyBackupPath(path1);
if (!fs.existsSync(backupFolder1)) {
fs.mkdirSync(backupFolder1);
fs.mkdirSync(path.join(backupFolder1, Schemas.file));
await pfs.writeFile(path.join(backupFolder1, Schemas.file, 'unsaved1.txt'), 'Legacy');
}
const backupFolder2 = service.toLegacyBackupPath(path2);
if (!fs.existsSync(backupFolder2)) {
fs.mkdirSync(backupFolder2);
fs.mkdirSync(path.join(backupFolder2, Schemas.file));
await pfs.writeFile(path.join(backupFolder2, Schemas.file, 'unsaved2.txt'), 'Legacy');
}
const workspacesJson = { rootWorkspaces: [], folderWorkspaces: [path1, path2], emptyWorkspaces: [] };
await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
await service.initialize();
const content = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepEqual(json.folderURIWorkspaces, [uri1.toString(), uri2.toString()]);
const newBackupFolder1 = service.toBackupPath(uri1);
assert.ok(fs.existsSync(path.join(newBackupFolder1, Schemas.file, 'unsaved1.txt')));
const newBackupFolder2 = service.toBackupPath(uri2);
assert.ok(fs.existsSync(path.join(newBackupFolder2, Schemas.file, 'unsaved2.txt')));
});
test('migrate storage file', async () => {
let folderPath = path.join(parentDir, 'f1');
ensureFolderExists(URI.file(folderPath));
const backupFolderPath = service.toLegacyBackupPath(folderPath);
await createBackupFolder(backupFolderPath);
let workspacePath = path.join(parentDir, 'f2.code-workspace');
const workspace = toWorkspace(workspacePath);
await ensureWorkspaceExists(workspace);
const workspacesJson = { rootWorkspaces: [{ id: workspace.id, configPath: workspacePath }], folderWorkspaces: [folderPath], emptyWorkspaces: [] };
await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
await service.initialize();
const content = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepEqual(json.folderURIWorkspaces, [URI.file(folderPath).toString()]);
assert.deepEqual(json.rootURIWorkspaces, [{ id: workspace.id, configURIPath: URI.file(workspacePath).toString() }]);
assertEqualUris(service.getWorkspaceBackups().map(window => window.workspace.configPath), [workspace.configPath]);
});
});
suite('loadSync', () => {
test('getFolderBackupPaths() should return [] when workspaces.json doesn\'t exist', () => {
assertEqualUris(service.getFolderBackupPaths(), []);
});
test('getFolderBackupPaths() should return [] when workspaces.json is not properly formed JSON', async () => {
fs.writeFileSync(backupWorkspacesPath, '');
await service.initialize();
assertEqualUris(service.getFolderBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{]');
await service.initialize();
assertEqualUris(service.getFolderBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, 'foo');
await service.initialize();
assertEqualUris(service.getFolderBackupPaths(), []);
});
test('getFolderBackupPaths() should return [] when folderWorkspaces in workspaces.json is absent', async () => {
fs.writeFileSync(backupWorkspacesPath, '{}');
await service.initialize();
assertEqualUris(service.getFolderBackupPaths(), []);
});
test('getFolderBackupPaths() should return [] when folderWorkspaces in workspaces.json is not a string array', async () => {
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{}}');
await service.initialize();
assertEqualUris(service.getFolderBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": ["bar"]}}');
await service.initialize();
assertEqualUris(service.getFolderBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": []}}');
await service.initialize();
assertEqualUris(service.getFolderBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": "bar"}}');
await service.initialize();
assertEqualUris(service.getFolderBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":"foo"}');
await service.initialize();
assertEqualUris(service.getFolderBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":1}');
await service.initialize();
assertEqualUris(service.getFolderBackupPaths(), []);
});
test('getFolderBackupPaths() should return [] when files.hotExit = "onExitAndWindowClose"', async () => {
service.registerFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase()));
assertEqualUris(service.getFolderBackupPaths(), [URI.file(fooFile.fsPath.toUpperCase())]);
configService.setUserConfiguration('files.hotExit', HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE);
await service.initialize();
assertEqualUris(service.getFolderBackupPaths(), []);
});
test('getWorkspaceBackups() should return [] when workspaces.json doesn\'t exist', () => {
assert.deepEqual(service.getWorkspaceBackups(), []);
});
test('getWorkspaceBackups() should return [] when workspaces.json is not properly formed JSON', async () => {
fs.writeFileSync(backupWorkspacesPath, '');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{]');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, 'foo');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
});
test('getWorkspaceBackups() should return [] when folderWorkspaces in workspaces.json is absent', async () => {
fs.writeFileSync(backupWorkspacesPath, '{}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
});
test('getWorkspaceBackups() should return [] when rootWorkspaces in workspaces.json is not a object array', async () => {
fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{}}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": ["bar"]}}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": []}}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": "bar"}}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":"foo"}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":1}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
});
test('getWorkspaceBackups() should return [] when rootURIWorkspaces in workspaces.json is not a object array', async () => {
fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{}}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{"foo": ["bar"]}}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{"foo": []}}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{"foo": "bar"}}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":"foo"}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":1}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
});
test('getWorkspaceBackups() should return [] when files.hotExit = "onExitAndWindowClose"', async () => {
const upperFooPath = fooFile.fsPath.toUpperCase();
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(upperFooPath));
assert.equal(service.getWorkspaceBackups().length, 1);
assertEqualUris(service.getWorkspaceBackups().map(r => r.workspace.configPath), [URI.file(upperFooPath)]);
configService.setUserConfiguration('files.hotExit', HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE);
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
});
test('getEmptyWorkspaceBackupPaths() should return [] when workspaces.json doesn\'t exist', () => {
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
});
test('getEmptyWorkspaceBackupPaths() should return [] when workspaces.json is not properly formed JSON', async () => {
fs.writeFileSync(backupWorkspacesPath, '');
await service.initialize();
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{]');
await service.initialize();
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, 'foo');
await service.initialize();
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
});
test('getEmptyWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is absent', async () => {
fs.writeFileSync(backupWorkspacesPath, '{}');
await service.initialize();
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
});
test('getEmptyWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is not a string array', async function () {
this.timeout(5000);
fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{}}');
await service.initialize();
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": ["bar"]}}');
await service.initialize();
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": []}}');
await service.initialize();
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": "bar"}}');
await service.initialize();
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":"foo"}');
await service.initialize();
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":1}');
await service.initialize();
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
});
});
suite('dedupeFolderWorkspaces', () => {
test('should ignore duplicates (folder workspace)', async () => {
await ensureFolderExists(existingTestFolder1);
const workspacesJson: IBackupWorkspacesFormat = {
rootURIWorkspaces: [],
folderURIWorkspaces: [existingTestFolder1.toString(), existingTestFolder1.toString()],
emptyWorkspaceInfos: []
};
await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
await service.initialize();
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
});
test('should ignore duplicates on Windows and Mac (folder workspace)', async () => {
await ensureFolderExists(existingTestFolder1);
const workspacesJson: IBackupWorkspacesFormat = {
rootURIWorkspaces: [],
folderURIWorkspaces: [existingTestFolder1.toString(), existingTestFolder1.toString().toLowerCase()],
emptyWorkspaceInfos: []
};
await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
await service.initialize();
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
});
test('should ignore duplicates on Windows and Mac (root workspace)', async () => {
const workspacePath = path.join(parentDir, 'Foo.code-workspace');
const workspacePath1 = path.join(parentDir, 'FOO.code-workspace');
const workspacePath2 = path.join(parentDir, 'foo.code-workspace');
const workspace1 = await ensureWorkspaceExists(toWorkspace(workspacePath));
const workspace2 = await ensureWorkspaceExists(toWorkspace(workspacePath1));
const workspace3 = await ensureWorkspaceExists(toWorkspace(workspacePath2));
const workspacesJson: IBackupWorkspacesFormat = {
rootURIWorkspaces: [workspace1, workspace2, workspace3].map(toSerializedWorkspace),
folderURIWorkspaces: [],
emptyWorkspaceInfos: []
};
await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
await service.initialize();
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.equal(json.rootURIWorkspaces.length, platform.isLinux ? 3 : 1);
if (platform.isLinux) {
assert.deepEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [URI.file(workspacePath).toString(), URI.file(workspacePath1).toString(), URI.file(workspacePath2).toString()]);
} else {
assert.deepEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [URI.file(workspacePath).toString()], 'should return the first duplicated entry');
}
});
});
suite('registerWindowForBackups', () => {
test('should persist paths to workspaces.json (folder workspace)', async () => {
service.registerFolderBackupSync(fooFile);
service.registerFolderBackupSync(barFile);
assertEqualUris(service.getFolderBackupPaths(), [fooFile, barFile]);
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.folderURIWorkspaces, [fooFile.toString(), barFile.toString()]);
});
test('should persist paths to workspaces.json (root workspace)', async () => {
const ws1 = toWorkspaceBackupInfo(fooFile.fsPath);
service.registerWorkspaceBackupSync(ws1);
const ws2 = toWorkspaceBackupInfo(barFile.fsPath);
service.registerWorkspaceBackupSync(ws2);
assertEqualUris(service.getWorkspaceBackups().map(b => b.workspace.configPath), [fooFile, barFile]);
assert.equal(ws1.workspace.id, service.getWorkspaceBackups()[0].workspace.id);
assert.equal(ws2.workspace.id, service.getWorkspaceBackups()[1].workspace.id);
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [fooFile.toString(), barFile.toString()]);
assert.equal(ws1.workspace.id, json.rootURIWorkspaces[0].id);
assert.equal(ws2.workspace.id, json.rootURIWorkspaces[1].id);
});
});
test('should always store the workspace path in workspaces.json using the case given, regardless of whether the file system is case-sensitive (folder workspace)', async () => {
service.registerFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase()));
assertEqualUris(service.getFolderBackupPaths(), [URI.file(fooFile.fsPath.toUpperCase())]);
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.folderURIWorkspaces, [URI.file(fooFile.fsPath.toUpperCase()).toString()]);
});
test('should always store the workspace path in workspaces.json using the case given, regardless of whether the file system is case-sensitive (root workspace)', async () => {
const upperFooPath = fooFile.fsPath.toUpperCase();
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(upperFooPath));
assertEqualUris(service.getWorkspaceBackups().map(b => b.workspace.configPath), [URI.file(upperFooPath)]);
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [URI.file(upperFooPath).toString()]);
});
suite('removeBackupPathSync', () => {
test('should remove folder workspaces from workspaces.json (folder workspace)', async () => {
service.registerFolderBackupSync(fooFile);
service.registerFolderBackupSync(barFile);
service.unregisterFolderBackupSync(fooFile);
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepEqual(json.folderURIWorkspaces, [barFile.toString()]);
service.unregisterFolderBackupSync(barFile);
const content = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json2 = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepEqual(json2.folderURIWorkspaces, []);
});
test('should remove folder workspaces from workspaces.json (root workspace)', async () => {
const ws1 = toWorkspaceBackupInfo(fooFile.fsPath);
service.registerWorkspaceBackupSync(ws1);
const ws2 = toWorkspaceBackupInfo(barFile.fsPath);
service.registerWorkspaceBackupSync(ws2);
service.unregisterWorkspaceBackupSync(ws1.workspace);
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [barFile.toString()]);
service.unregisterWorkspaceBackupSync(ws2.workspace);
const content = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json2 = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepEqual(json2.rootURIWorkspaces, []);
});
test('should remove empty workspaces from workspaces.json', async () => {
service.registerEmptyWindowBackupSync('foo');
service.registerEmptyWindowBackupSync('bar');
service.unregisterEmptyWindowBackupSync('foo');
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepEqual(json.emptyWorkspaces, ['bar']);
service.unregisterEmptyWindowBackupSync('bar');
const content = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json2 = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepEqual(json2.emptyWorkspaces, []);
});
test('should fail gracefully when removing a path that doesn\'t exist', async () => {
await ensureFolderExists(existingTestFolder1); // make sure backup folder exists, so the folder is not removed on loadSync
const workspacesJson: IBackupWorkspacesFormat = { rootURIWorkspaces: [], folderURIWorkspaces: [existingTestFolder1.toString()], emptyWorkspaceInfos: [] };
await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
await service.initialize();
service.unregisterFolderBackupSync(barFile);
service.unregisterEmptyWindowBackupSync('test');
const content = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
});
});
suite('getWorkspaceHash', () => {
test('should ignore case on Windows and Mac', () => {
// Skip test on Linux
if (platform.isLinux) {
return;
}
if (platform.isMacintosh) {
assert.equal(service.getFolderHash(URI.file('/foo')), service.getFolderHash(URI.file('/FOO')));
}
if (platform.isWindows) {
assert.equal(service.getFolderHash(URI.file('c:\\foo')), service.getFolderHash(URI.file('C:\\FOO')));
}
});
});
suite('mixed path casing', () => {
test('should handle case insensitive paths properly (registerWindowForBackupsSync) (folder workspace)', () => {
service.registerFolderBackupSync(fooFile);
service.registerFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase()));
if (platform.isLinux) {
assert.equal(service.getFolderBackupPaths().length, 2);
} else {
assert.equal(service.getFolderBackupPaths().length, 1);
}
});
test('should handle case insensitive paths properly (registerWindowForBackupsSync) (root workspace)', () => {
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath));
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath.toUpperCase()));
if (platform.isLinux) {
assert.equal(service.getWorkspaceBackups().length, 2);
} else {
assert.equal(service.getWorkspaceBackups().length, 1);
}
});
test('should handle case insensitive paths properly (removeBackupPathSync) (folder workspace)', () => {
// same case
service.registerFolderBackupSync(fooFile);
service.unregisterFolderBackupSync(fooFile);
assert.equal(service.getFolderBackupPaths().length, 0);
// mixed case
service.registerFolderBackupSync(fooFile);
service.unregisterFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase()));
if (platform.isLinux) {
assert.equal(service.getFolderBackupPaths().length, 1);
} else {
assert.equal(service.getFolderBackupPaths().length, 0);
}
});
});
suite('getDirtyWorkspaces', () => {
test('should report if a workspace or folder has backups', async () => {
const folderBackupPath = service.registerFolderBackupSync(fooFile);
const backupWorkspaceInfo = toWorkspaceBackupInfo(fooFile.fsPath);
const workspaceBackupPath = service.registerWorkspaceBackupSync(backupWorkspaceInfo);
assert.equal(((await service.getDirtyWorkspaces()).length), 0);
try {
await pfs.mkdirp(path.join(folderBackupPath, Schemas.file));
await pfs.mkdirp(path.join(workspaceBackupPath, Schemas.untitled));
} catch (error) {
// ignore - folder might exist already
}
assert.equal(((await service.getDirtyWorkspaces()).length), 0);
fs.writeFileSync(path.join(folderBackupPath, Schemas.file, '594a4a9d82a277a899d4713a5b08f504'), '');
fs.writeFileSync(path.join(workspaceBackupPath, Schemas.untitled, '594a4a9d82a277a899d4713a5b08f504'), '');
const dirtyWorkspaces = await service.getDirtyWorkspaces();
assert.equal(dirtyWorkspaces.length, 2);
let found = 0;
for (const dirtyWorkpspace of dirtyWorkspaces) {
if (URI.isUri(dirtyWorkpspace)) {
if (isEqual(fooFile, dirtyWorkpspace)) {
found++;
}
} else {
if (isEqual(backupWorkspaceInfo.workspace.configPath, dirtyWorkpspace.configPath)) {
found++;
}
}
}
assert.equal(found, 2);
});
});
});

View File

@@ -0,0 +1,105 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey, IContextKeyServiceTarget } from 'vs/platform/contextkey/common/contextkey';
import { HistoryInputBox, IHistoryInputOptions } from 'vs/base/browser/ui/inputbox/inputBox';
import { FindInput, IFindInputOptions } from 'vs/base/browser/ui/findinput/findInput';
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
import { IHistoryNavigationWidget } from 'vs/base/browser/history';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { ReplaceInput, IReplaceInputOptions } from 'vs/base/browser/ui/findinput/replaceInput';
export const HistoryNavigationWidgetContext = 'historyNavigationWidget';
export const HistoryNavigationEnablementContext = 'historyNavigationEnabled';
function bindContextScopedWidget(contextKeyService: IContextKeyService, widget: IContextScopedWidget, contextKey: string): void {
new RawContextKey<IContextScopedWidget>(contextKey, widget).bindTo(contextKeyService);
}
function createWidgetScopedContextKeyService(contextKeyService: IContextKeyService, widget: IContextScopedWidget): IContextKeyService {
return contextKeyService.createScoped(widget.target);
}
function getContextScopedWidget<T extends IContextScopedWidget>(contextKeyService: IContextKeyService, contextKey: string): T | undefined {
return contextKeyService.getContext(document.activeElement).getValue(contextKey);
}
interface IContextScopedWidget {
readonly target: IContextKeyServiceTarget;
}
interface IContextScopedHistoryNavigationWidget extends IContextScopedWidget {
historyNavigator: IHistoryNavigationWidget;
}
export function createAndBindHistoryNavigationWidgetScopedContextKeyService(contextKeyService: IContextKeyService, widget: IContextScopedHistoryNavigationWidget): { scopedContextKeyService: IContextKeyService, historyNavigationEnablement: IContextKey<boolean> } {
const scopedContextKeyService = createWidgetScopedContextKeyService(contextKeyService, widget);
bindContextScopedWidget(scopedContextKeyService, widget, HistoryNavigationWidgetContext);
const historyNavigationEnablement = new RawContextKey<boolean>(HistoryNavigationEnablementContext, true).bindTo(scopedContextKeyService);
return { scopedContextKeyService, historyNavigationEnablement };
}
export class ContextScopedHistoryInputBox extends HistoryInputBox {
constructor(container: HTMLElement, contextViewProvider: IContextViewProvider | undefined, options: IHistoryInputOptions,
@IContextKeyService contextKeyService: IContextKeyService
) {
super(container, contextViewProvider, options);
this._register(createAndBindHistoryNavigationWidgetScopedContextKeyService(contextKeyService, <IContextScopedHistoryNavigationWidget>{ target: this.element, historyNavigator: this }).scopedContextKeyService);
}
}
export class ContextScopedFindInput extends FindInput {
constructor(container: HTMLElement | null, contextViewProvider: IContextViewProvider, options: IFindInputOptions,
@IContextKeyService contextKeyService: IContextKeyService, showFindOptions: boolean = false
) {
super(container, contextViewProvider, showFindOptions, options);
this._register(createAndBindHistoryNavigationWidgetScopedContextKeyService(contextKeyService, <IContextScopedHistoryNavigationWidget>{ target: this.inputBox.element, historyNavigator: this.inputBox }).scopedContextKeyService);
}
}
export class ContextScopedReplaceInput extends ReplaceInput {
constructor(container: HTMLElement | null, contextViewProvider: IContextViewProvider | undefined, options: IReplaceInputOptions,
@IContextKeyService contextKeyService: IContextKeyService, showReplaceOptions: boolean = false
) {
super(container, contextViewProvider, showReplaceOptions, options);
this._register(createAndBindHistoryNavigationWidgetScopedContextKeyService(contextKeyService, <IContextScopedHistoryNavigationWidget>{ target: this.inputBox.element, historyNavigator: this.inputBox }).scopedContextKeyService);
}
}
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'history.showPrevious',
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(ContextKeyExpr.has(HistoryNavigationWidgetContext), ContextKeyExpr.equals(HistoryNavigationEnablementContext, true)),
primary: KeyCode.UpArrow,
secondary: [KeyMod.Alt | KeyCode.UpArrow],
handler: (accessor, arg2) => {
const widget = getContextScopedWidget<IContextScopedHistoryNavigationWidget>(accessor.get(IContextKeyService), HistoryNavigationWidgetContext);
if (widget) {
const historyInputBox: IHistoryNavigationWidget = widget.historyNavigator;
historyInputBox.showPreviousValue();
}
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'history.showNext',
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(ContextKeyExpr.has(HistoryNavigationWidgetContext), ContextKeyExpr.equals(HistoryNavigationEnablementContext, true)),
primary: KeyCode.DownArrow,
secondary: [KeyMod.Alt | KeyCode.DownArrow],
handler: (accessor, arg2) => {
const widget = getContextScopedWidget<IContextScopedHistoryNavigationWidget>(accessor.get(IContextKeyService), HistoryNavigationWidgetContext);
if (widget) {
const historyInputBox: IHistoryNavigationWidget = widget.historyNavigator;
historyInputBox.showNextValue();
}
}
});

View File

@@ -0,0 +1,100 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { URI } from 'vs/base/common/uri';
import { $ } from 'vs/base/browser/dom';
export class BrowserClipboardService implements IClipboardService {
declare readonly _serviceBrand: undefined;
private readonly mapTextToType = new Map<string, string>(); // unsupported in web (only in-memory)
async writeText(text: string, type?: string): Promise<void> {
// With type: only in-memory is supported
if (type) {
this.mapTextToType.set(type, text);
return;
}
// Guard access to navigator.clipboard with try/catch
// as we have seen DOMExceptions in certain browsers
// due to security policies.
try {
return await navigator.clipboard.writeText(text);
} catch (error) {
console.error(error);
}
// Fallback to textarea and execCommand solution
const activeElement = document.activeElement;
const textArea: HTMLTextAreaElement = document.body.appendChild($('textarea', { 'aria-hidden': true }));
textArea.style.height = '1px';
textArea.style.width = '1px';
textArea.style.position = 'absolute';
textArea.value = text;
textArea.focus();
textArea.select();
document.execCommand('copy');
if (activeElement instanceof HTMLElement) {
activeElement.focus();
}
document.body.removeChild(textArea);
return;
}
async readText(type?: string): Promise<string> {
// With type: only in-memory is supported
if (type) {
return this.mapTextToType.get(type) || '';
}
// Guard access to navigator.clipboard with try/catch
// as we have seen DOMExceptions in certain browsers
// due to security policies.
try {
return await navigator.clipboard.readText();
} catch (error) {
console.error(error);
return '';
}
}
private findText = ''; // unsupported in web (only in-memory)
async readFindText(): Promise<string> {
return this.findText;
}
async writeFindText(text: string): Promise<void> {
this.findText = text;
}
private resources: URI[] = []; // unsupported in web (only in-memory)
async writeResources(resources: URI[]): Promise<void> {
this.resources = resources;
}
async readResources(): Promise<URI[]> {
return this.resources;
}
async hasResources(): Promise<boolean> {
return this.resources.length > 0;
}
}

View File

@@ -0,0 +1,49 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { URI } from 'vs/base/common/uri';
export const IClipboardService = createDecorator<IClipboardService>('clipboardService');
export interface IClipboardService {
readonly _serviceBrand: undefined;
/**
* Writes text to the system clipboard.
*/
writeText(text: string, type?: string): Promise<void>;
/**
* Reads the content of the clipboard in plain text
*/
readText(type?: string): Promise<string>;
/**
* Reads text from the system find pasteboard.
*/
readFindText(): Promise<string>;
/**
* Writes text to the system find pasteboard.
*/
writeFindText(text: string): Promise<void>;
/**
* Writes resources to the system clipboard.
*/
writeResources(resources: URI[]): Promise<void>;
/**
* Reads resources from the system clipboard.
*/
readResources(): Promise<URI[]>;
/**
* Find out if resources are copied to the clipboard.
*/
hasResources(): Promise<boolean>;
}

View File

@@ -0,0 +1,149 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle';
import { TypeConstraint, validateConstraints } from 'vs/base/common/types';
import { ServicesAccessor, createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event, Emitter } from 'vs/base/common/event';
import { LinkedList } from 'vs/base/common/linkedList';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { Iterable } from 'vs/base/common/iterator';
export const ICommandService = createDecorator<ICommandService>('commandService');
export interface ICommandEvent {
commandId: string;
args: any[];
}
export interface ICommandService {
readonly _serviceBrand: undefined;
onWillExecuteCommand: Event<ICommandEvent>;
onDidExecuteCommand: Event<ICommandEvent>;
executeCommand<T = any>(commandId: string, ...args: any[]): Promise<T | undefined>;
}
export type ICommandsMap = Map<string, ICommand>;
export interface ICommandHandler {
(accessor: ServicesAccessor, ...args: any[]): void;
}
export interface ICommand {
id: string;
handler: ICommandHandler;
description?: ICommandHandlerDescription | null;
}
export interface ICommandHandlerDescription {
readonly description: string;
readonly args: ReadonlyArray<{
readonly name: string;
readonly description?: string;
readonly constraint?: TypeConstraint;
readonly schema?: IJSONSchema;
}>;
readonly returns?: string;
}
export interface ICommandRegistry {
onDidRegisterCommand: Event<string>;
registerCommand(id: string, command: ICommandHandler): IDisposable;
registerCommand(command: ICommand): IDisposable;
registerCommandAlias(oldId: string, newId: string): IDisposable;
getCommand(id: string): ICommand | undefined;
getCommands(): ICommandsMap;
}
export const CommandsRegistry: ICommandRegistry = new class implements ICommandRegistry {
private readonly _commands = new Map<string, LinkedList<ICommand>>();
private readonly _onDidRegisterCommand = new Emitter<string>();
readonly onDidRegisterCommand: Event<string> = this._onDidRegisterCommand.event;
registerCommand(idOrCommand: string | ICommand, handler?: ICommandHandler): IDisposable {
if (!idOrCommand) {
throw new Error(`invalid command`);
}
if (typeof idOrCommand === 'string') {
if (!handler) {
throw new Error(`invalid command`);
}
return this.registerCommand({ id: idOrCommand, handler });
}
// add argument validation if rich command metadata is provided
if (idOrCommand.description) {
const constraints: Array<TypeConstraint | undefined> = [];
for (let arg of idOrCommand.description.args) {
constraints.push(arg.constraint);
}
const actualHandler = idOrCommand.handler;
idOrCommand.handler = function (accessor, ...args: any[]) {
validateConstraints(args, constraints);
return actualHandler(accessor, ...args);
};
}
// find a place to store the command
const { id } = idOrCommand;
let commands = this._commands.get(id);
if (!commands) {
commands = new LinkedList<ICommand>();
this._commands.set(id, commands);
}
let removeFn = commands.unshift(idOrCommand);
let ret = toDisposable(() => {
removeFn();
const command = this._commands.get(id);
if (command?.isEmpty()) {
this._commands.delete(id);
}
});
// tell the world about this command
this._onDidRegisterCommand.fire(id);
return ret;
}
registerCommandAlias(oldId: string, newId: string): IDisposable {
return CommandsRegistry.registerCommand(oldId, (accessor, ...args) => accessor.get(ICommandService).executeCommand(newId, ...args));
}
getCommand(id: string): ICommand | undefined {
const list = this._commands.get(id);
if (!list || list.isEmpty()) {
return undefined;
}
return Iterable.first(list);
}
getCommands(): ICommandsMap {
const result = new Map<string, ICommand>();
for (const key of this._commands.keys()) {
const command = this.getCommand(key);
if (command) {
result.set(key, command);
}
}
return result;
}
};
export const NullCommandService: ICommandService = {
_serviceBrand: undefined,
onWillExecuteCommand: () => Disposable.None,
onDidExecuteCommand: () => Disposable.None,
executeCommand() {
return Promise.resolve(undefined);
}
};

View File

@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
suite('Command Tests', function () {
test('register command - no handler', function () {
assert.throws(() => CommandsRegistry.registerCommand('foo', null!));
});
test('register/dispose', () => {
const command = function () { };
const reg = CommandsRegistry.registerCommand('foo', command);
assert.ok(CommandsRegistry.getCommand('foo')!.handler === command);
reg.dispose();
assert.ok(CommandsRegistry.getCommand('foo') === undefined);
});
test('register/register/dispose', () => {
const command1 = function () { };
const command2 = function () { };
// dispose overriding command
let reg1 = CommandsRegistry.registerCommand('foo', command1);
assert.ok(CommandsRegistry.getCommand('foo')!.handler === command1);
let reg2 = CommandsRegistry.registerCommand('foo', command2);
assert.ok(CommandsRegistry.getCommand('foo')!.handler === command2);
reg2.dispose();
assert.ok(CommandsRegistry.getCommand('foo')!.handler === command1);
reg1.dispose();
assert.ok(CommandsRegistry.getCommand('foo') === undefined);
// dispose override command first
reg1 = CommandsRegistry.registerCommand('foo', command1);
reg2 = CommandsRegistry.registerCommand('foo', command2);
assert.ok(CommandsRegistry.getCommand('foo')!.handler === command2);
reg1.dispose();
assert.ok(CommandsRegistry.getCommand('foo')!.handler === command2);
reg2.dispose();
assert.ok(CommandsRegistry.getCommand('foo') === undefined);
});
test('command with description', function () {
CommandsRegistry.registerCommand('test', function (accessor, args) {
assert.ok(typeof args === 'string');
});
CommandsRegistry.registerCommand('test2', function (accessor, args) {
assert.ok(typeof args === 'string');
});
CommandsRegistry.registerCommand({
id: 'test3',
handler: function (accessor, args) {
return true;
},
description: {
description: 'a command',
args: [{ name: 'value', constraint: Number }]
}
});
CommandsRegistry.getCommands().get('test')!.handler.apply(undefined, [undefined!, 'string']);
CommandsRegistry.getCommands().get('test2')!.handler.apply(undefined, [undefined!, 'string']);
assert.throws(() => CommandsRegistry.getCommands().get('test3')!.handler.apply(undefined, [undefined!, 'string']));
assert.equal(CommandsRegistry.getCommands().get('test3')!.handler.apply(undefined, [undefined!, 1]), true);
});
});

View File

@@ -0,0 +1,372 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as objects from 'vs/base/common/objects';
import * as types from 'vs/base/common/types';
import { URI, UriComponents } from 'vs/base/common/uri';
import { Event } from 'vs/base/common/event';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IConfigurationRegistry, Extensions, OVERRIDE_PROPERTY_PATTERN, overrideIdentifierFromKey } from 'vs/platform/configuration/common/configurationRegistry';
import { IStringDictionary } from 'vs/base/common/collections';
export const IConfigurationService = createDecorator<IConfigurationService>('configurationService');
export function isConfigurationOverrides(thing: any): thing is IConfigurationOverrides {
return thing
&& typeof thing === 'object'
&& (!thing.overrideIdentifier || typeof thing.overrideIdentifier === 'string')
&& (!thing.resource || thing.resource instanceof URI);
}
export interface IConfigurationOverrides {
overrideIdentifier?: string | null;
resource?: URI | null;
}
export const enum ConfigurationTarget {
USER = 1,
USER_LOCAL,
USER_REMOTE,
WORKSPACE,
WORKSPACE_FOLDER,
DEFAULT,
MEMORY
}
export function ConfigurationTargetToString(configurationTarget: ConfigurationTarget) {
switch (configurationTarget) {
case ConfigurationTarget.USER: return 'USER';
case ConfigurationTarget.USER_LOCAL: return 'USER_LOCAL';
case ConfigurationTarget.USER_REMOTE: return 'USER_REMOTE';
case ConfigurationTarget.WORKSPACE: return 'WORKSPACE';
case ConfigurationTarget.WORKSPACE_FOLDER: return 'WORKSPACE_FOLDER';
case ConfigurationTarget.DEFAULT: return 'DEFAULT';
case ConfigurationTarget.MEMORY: return 'MEMORY';
}
}
export interface IConfigurationChange {
keys: string[];
overrides: [string, string[]][];
}
export interface IConfigurationChangeEvent {
readonly source: ConfigurationTarget;
readonly affectedKeys: string[];
readonly change: IConfigurationChange;
affectsConfiguration(configuration: string, overrides?: IConfigurationOverrides): boolean;
// Following data is used for telemetry
readonly sourceConfig: any;
}
export interface IConfigurationValue<T> {
readonly defaultValue?: T;
readonly userValue?: T;
readonly userLocalValue?: T;
readonly userRemoteValue?: T;
readonly workspaceValue?: T;
readonly workspaceFolderValue?: T;
readonly memoryValue?: T;
readonly value?: T;
readonly default?: { value?: T, override?: T };
readonly user?: { value?: T, override?: T };
readonly userLocal?: { value?: T, override?: T };
readonly userRemote?: { value?: T, override?: T };
readonly workspace?: { value?: T, override?: T };
readonly workspaceFolder?: { value?: T, override?: T };
readonly memory?: { value?: T, override?: T };
readonly overrideIdentifiers?: string[];
}
export interface IConfigurationService {
readonly _serviceBrand: undefined;
onDidChangeConfiguration: Event<IConfigurationChangeEvent>;
getConfigurationData(): IConfigurationData | null;
/**
* Fetches the value of the section for the given overrides.
* Value can be of native type or an object keyed off the section name.
*
* @param section - Section of the configuraion. Can be `null` or `undefined`.
* @param overrides - Overrides that has to be applied while fetching
*
*/
getValue<T>(): T;
getValue<T>(section: string): T;
getValue<T>(overrides: IConfigurationOverrides): T;
getValue<T>(section: string, overrides: IConfigurationOverrides): T;
updateValue(key: string, value: any): Promise<void>;
updateValue(key: string, value: any, overrides: IConfigurationOverrides): Promise<void>;
updateValue(key: string, value: any, target: ConfigurationTarget): Promise<void>;
updateValue(key: string, value: any, overrides: IConfigurationOverrides, target: ConfigurationTarget, donotNotifyError?: boolean): Promise<void>;
inspect<T>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<T>;
reloadConfiguration(folder?: IWorkspaceFolder): Promise<void>;
keys(): {
default: string[];
user: string[];
workspace: string[];
workspaceFolder: string[];
memory?: string[];
};
}
export interface IConfigurationModel {
contents: any;
keys: string[];
overrides: IOverrides[];
}
export interface IOverrides {
keys: string[];
contents: any;
identifiers: string[];
}
export interface IConfigurationData {
defaults: IConfigurationModel;
user: IConfigurationModel;
workspace: IConfigurationModel;
folders: [UriComponents, IConfigurationModel][];
}
export interface IConfigurationCompareResult {
added: string[];
removed: string[];
updated: string[];
overrides: [string, string[]][];
}
export function compare(from: IConfigurationModel | undefined, to: IConfigurationModel | undefined): IConfigurationCompareResult {
const added = to
? from ? to.keys.filter(key => from.keys.indexOf(key) === -1) : [...to.keys]
: [];
const removed = from
? to ? from.keys.filter(key => to.keys.indexOf(key) === -1) : [...from.keys]
: [];
const updated: string[] = [];
if (to && from) {
for (const key of from.keys) {
if (to.keys.indexOf(key) !== -1) {
const value1 = getConfigurationValue(from.contents, key);
const value2 = getConfigurationValue(to.contents, key);
if (!objects.equals(value1, value2)) {
updated.push(key);
}
}
}
}
const overrides: [string, string[]][] = [];
const byOverrideIdentifier = (overrides: IOverrides[]): IStringDictionary<IOverrides> => {
const result: IStringDictionary<IOverrides> = {};
for (const override of overrides) {
for (const identifier of override.identifiers) {
result[keyFromOverrideIdentifier(identifier)] = override;
}
}
return result;
};
const toOverridesByIdentifier: IStringDictionary<IOverrides> = to ? byOverrideIdentifier(to.overrides) : {};
const fromOverridesByIdentifier: IStringDictionary<IOverrides> = from ? byOverrideIdentifier(from.overrides) : {};
if (Object.keys(toOverridesByIdentifier).length) {
for (const key of added) {
const override = toOverridesByIdentifier[key];
if (override) {
overrides.push([overrideIdentifierFromKey(key), override.keys]);
}
}
}
if (Object.keys(fromOverridesByIdentifier).length) {
for (const key of removed) {
const override = fromOverridesByIdentifier[key];
if (override) {
overrides.push([overrideIdentifierFromKey(key), override.keys]);
}
}
}
if (Object.keys(toOverridesByIdentifier).length && Object.keys(fromOverridesByIdentifier).length) {
for (const key of updated) {
const fromOverride = fromOverridesByIdentifier[key];
const toOverride = toOverridesByIdentifier[key];
if (fromOverride && toOverride) {
const result = compare({ contents: fromOverride.contents, keys: fromOverride.keys, overrides: [] }, { contents: toOverride.contents, keys: toOverride.keys, overrides: [] });
overrides.push([overrideIdentifierFromKey(key), [...result.added, ...result.removed, ...result.updated]]);
}
}
}
return { added, removed, updated, overrides };
}
export function toOverrides(raw: any, conflictReporter: (message: string) => void): IOverrides[] {
const overrides: IOverrides[] = [];
for (const key of Object.keys(raw)) {
if (OVERRIDE_PROPERTY_PATTERN.test(key)) {
const overrideRaw: any = {};
for (const keyInOverrideRaw in raw[key]) {
overrideRaw[keyInOverrideRaw] = raw[key][keyInOverrideRaw];
}
overrides.push({
identifiers: [overrideIdentifierFromKey(key).trim()],
keys: Object.keys(overrideRaw),
contents: toValuesTree(overrideRaw, conflictReporter)
});
}
}
return overrides;
}
export function toValuesTree(properties: { [qualifiedKey: string]: any }, conflictReporter: (message: string) => void): any {
const root = Object.create(null);
for (let key in properties) {
addToValueTree(root, key, properties[key], conflictReporter);
}
return root;
}
export function addToValueTree(settingsTreeRoot: any, key: string, value: any, conflictReporter: (message: string) => void): void {
const segments = key.split('.');
const last = segments.pop()!;
let curr = settingsTreeRoot;
for (let i = 0; i < segments.length; i++) {
let s = segments[i];
let obj = curr[s];
switch (typeof obj) {
case 'undefined':
obj = curr[s] = Object.create(null);
break;
case 'object':
break;
default:
conflictReporter(`Ignoring ${key} as ${segments.slice(0, i + 1).join('.')} is ${JSON.stringify(obj)}`);
return;
}
curr = obj;
}
if (typeof curr === 'object' && curr !== null) {
try {
curr[last] = value; // workaround https://github.com/microsoft/vscode/issues/13606
} catch (e) {
conflictReporter(`Ignoring ${key} as ${segments.join('.')} is ${JSON.stringify(curr)}`);
}
} else {
conflictReporter(`Ignoring ${key} as ${segments.join('.')} is ${JSON.stringify(curr)}`);
}
}
export function removeFromValueTree(valueTree: any, key: string): void {
const segments = key.split('.');
doRemoveFromValueTree(valueTree, segments);
}
function doRemoveFromValueTree(valueTree: any, segments: string[]): void {
const first = segments.shift()!;
if (segments.length === 0) {
// Reached last segment
delete valueTree[first];
return;
}
if (Object.keys(valueTree).indexOf(first) !== -1) {
const value = valueTree[first];
if (typeof value === 'object' && !Array.isArray(value)) {
doRemoveFromValueTree(value, segments);
if (Object.keys(value).length === 0) {
delete valueTree[first];
}
}
}
}
/**
* A helper function to get the configuration value with a specific settings path (e.g. config.some.setting)
*/
export function getConfigurationValue<T>(config: any, settingPath: string, defaultValue?: T): T {
function accessSetting(config: any, path: string[]): any {
let current = config;
for (const component of path) {
if (typeof current !== 'object' || current === null) {
return undefined;
}
current = current[component];
}
return <T>current;
}
const path = settingPath.split('.');
const result = accessSetting(config, path);
return typeof result === 'undefined' ? defaultValue : result;
}
export function merge(base: any, add: any, overwrite: boolean): void {
Object.keys(add).forEach(key => {
if (key !== '__proto__') {
if (key in base) {
if (types.isObject(base[key]) && types.isObject(add[key])) {
merge(base[key], add[key], overwrite);
} else if (overwrite) {
base[key] = add[key];
}
} else {
base[key] = add[key];
}
}
});
}
export function getConfigurationKeys(): string[] {
const properties = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties();
return Object.keys(properties);
}
export function getDefaultValues(): any {
const valueTreeRoot: any = Object.create(null);
const properties = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties();
for (let key in properties) {
let value = properties[key].default;
addToValueTree(valueTreeRoot, key, value, message => console.error(`Conflict in default settings: ${message}`));
}
return valueTreeRoot;
}
export function keyFromOverrideIdentifier(overrideIdentifier: string): string {
return `[${overrideIdentifier}]`;
}
export function getMigratedSettingValue<T>(configurationService: IConfigurationService, currentSettingName: string, legacySettingName: string): T {
const setting = configurationService.inspect<T>(currentSettingName);
const legacySetting = configurationService.inspect<T>(legacySettingName);
if (typeof setting.userValue !== 'undefined' || typeof setting.workspaceValue !== 'undefined' || typeof setting.workspaceFolderValue !== 'undefined') {
return setting.value!;
} else if (typeof legacySetting.userValue !== 'undefined' || typeof legacySetting.workspaceValue !== 'undefined' || typeof legacySetting.workspaceFolderValue !== 'undefined') {
return legacySetting.value!;
} else {
return setting.defaultValue!;
}
}

View File

@@ -0,0 +1,803 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as json from 'vs/base/common/json';
import { ResourceMap, getOrSet } from 'vs/base/common/map';
import * as arrays from 'vs/base/common/arrays';
import * as types from 'vs/base/common/types';
import * as objects from 'vs/base/common/objects';
import { URI, UriComponents } from 'vs/base/common/uri';
import { OVERRIDE_PROPERTY_PATTERN, ConfigurationScope, IConfigurationRegistry, Extensions, IConfigurationPropertySchema, overrideIdentifierFromKey } from 'vs/platform/configuration/common/configurationRegistry';
import { IOverrides, addToValueTree, toValuesTree, IConfigurationModel, getConfigurationValue, IConfigurationOverrides, IConfigurationData, getDefaultValues, getConfigurationKeys, removeFromValueTree, toOverrides, IConfigurationValue, ConfigurationTarget, compare, IConfigurationChangeEvent, IConfigurationChange } from 'vs/platform/configuration/common/configuration';
import { Workspace } from 'vs/platform/workspace/common/workspace';
import { Registry } from 'vs/platform/registry/common/platform';
import { Disposable } from 'vs/base/common/lifecycle';
import { Emitter, Event } from 'vs/base/common/event';
import { IFileService } from 'vs/platform/files/common/files';
import { dirname } from 'vs/base/common/resources';
export class ConfigurationModel implements IConfigurationModel {
private isFrozen: boolean = false;
constructor(
private _contents: any = {},
private _keys: string[] = [],
private _overrides: IOverrides[] = []
) {
}
get contents(): any {
return this.checkAndFreeze(this._contents);
}
get overrides(): IOverrides[] {
return this.checkAndFreeze(this._overrides);
}
get keys(): string[] {
return this.checkAndFreeze(this._keys);
}
isEmpty(): boolean {
return this._keys.length === 0 && Object.keys(this._contents).length === 0 && this._overrides.length === 0;
}
getValue<V>(section: string | undefined): V {
return section ? getConfigurationValue<any>(this.contents, section) : this.contents;
}
getOverrideValue<V>(section: string | undefined, overrideIdentifier: string): V | undefined {
const overrideContents = this.getContentsForOverrideIdentifer(overrideIdentifier);
return overrideContents
? section ? getConfigurationValue<any>(overrideContents, section) : overrideContents
: undefined;
}
getKeysForOverrideIdentifier(identifier: string): string[] {
for (const override of this.overrides) {
if (override.identifiers.indexOf(identifier) !== -1) {
return override.keys;
}
}
return [];
}
override(identifier: string): ConfigurationModel {
const overrideContents = this.getContentsForOverrideIdentifer(identifier);
if (!overrideContents || typeof overrideContents !== 'object' || !Object.keys(overrideContents).length) {
// If there are no valid overrides, return self
return this;
}
let contents: any = {};
for (const key of arrays.distinct([...Object.keys(this.contents), ...Object.keys(overrideContents)])) {
let contentsForKey = this.contents[key];
let overrideContentsForKey = overrideContents[key];
// If there are override contents for the key, clone and merge otherwise use base contents
if (overrideContentsForKey) {
// Clone and merge only if base contents and override contents are of type object otherwise just override
if (typeof contentsForKey === 'object' && typeof overrideContentsForKey === 'object') {
contentsForKey = objects.deepClone(contentsForKey);
this.mergeContents(contentsForKey, overrideContentsForKey);
} else {
contentsForKey = overrideContentsForKey;
}
}
contents[key] = contentsForKey;
}
return new ConfigurationModel(contents, this.keys, this.overrides);
}
merge(...others: ConfigurationModel[]): ConfigurationModel {
const contents = objects.deepClone(this.contents);
const overrides = objects.deepClone(this.overrides);
const keys = [...this.keys];
for (const other of others) {
this.mergeContents(contents, other.contents);
for (const otherOverride of other.overrides) {
const [override] = overrides.filter(o => arrays.equals(o.identifiers, otherOverride.identifiers));
if (override) {
this.mergeContents(override.contents, otherOverride.contents);
} else {
overrides.push(objects.deepClone(otherOverride));
}
}
for (const key of other.keys) {
if (keys.indexOf(key) === -1) {
keys.push(key);
}
}
}
return new ConfigurationModel(contents, keys, overrides);
}
freeze(): ConfigurationModel {
this.isFrozen = true;
return this;
}
private mergeContents(source: any, target: any): void {
for (const key of Object.keys(target)) {
if (key in source) {
if (types.isObject(source[key]) && types.isObject(target[key])) {
this.mergeContents(source[key], target[key]);
continue;
}
}
source[key] = objects.deepClone(target[key]);
}
}
private checkAndFreeze<T>(data: T): T {
if (this.isFrozen && !Object.isFrozen(data)) {
return objects.deepFreeze(data);
}
return data;
}
private getContentsForOverrideIdentifer(identifier: string): any {
for (const override of this.overrides) {
if (override.identifiers.indexOf(identifier) !== -1) {
return override.contents;
}
}
return null;
}
toJSON(): IConfigurationModel {
return {
contents: this.contents,
overrides: this.overrides,
keys: this.keys
};
}
// Update methods
public setValue(key: string, value: any) {
this.addKey(key);
addToValueTree(this.contents, key, value, e => { throw new Error(e); });
}
public removeValue(key: string): void {
if (this.removeKey(key)) {
removeFromValueTree(this.contents, key);
}
}
private addKey(key: string): void {
let index = this.keys.length;
for (let i = 0; i < index; i++) {
if (key.indexOf(this.keys[i]) === 0) {
index = i;
}
}
this.keys.splice(index, 1, key);
}
private removeKey(key: string): boolean {
let index = this.keys.indexOf(key);
if (index !== -1) {
this.keys.splice(index, 1);
return true;
}
return false;
}
}
export class DefaultConfigurationModel extends ConfigurationModel {
constructor() {
const contents = getDefaultValues();
const keys = getConfigurationKeys();
const overrides: IOverrides[] = [];
for (const key of Object.keys(contents)) {
if (OVERRIDE_PROPERTY_PATTERN.test(key)) {
overrides.push({
identifiers: [overrideIdentifierFromKey(key).trim()],
keys: Object.keys(contents[key]),
contents: toValuesTree(contents[key], message => console.error(`Conflict in default settings file: ${message}`)),
});
}
}
super(contents, keys, overrides);
}
}
export class ConfigurationModelParser {
private _raw: any = null;
private _configurationModel: ConfigurationModel | null = null;
private _parseErrors: any[] = [];
constructor(protected readonly _name: string, private _scopes?: ConfigurationScope[]) { }
get configurationModel(): ConfigurationModel {
return this._configurationModel || new ConfigurationModel();
}
get errors(): any[] {
return this._parseErrors;
}
public parseContent(content: string | null | undefined): void {
if (!types.isUndefinedOrNull(content)) {
const raw = this.doParseContent(content);
this.parseRaw(raw);
}
}
public parseRaw(raw: any): void {
this._raw = raw;
const configurationModel = this.doParseRaw(raw);
this._configurationModel = new ConfigurationModel(configurationModel.contents, configurationModel.keys, configurationModel.overrides);
}
public parse(): void {
if (this._raw) {
this.parseRaw(this._raw);
}
}
protected doParseContent(content: string): any {
let raw: any = {};
let currentProperty: string | null = null;
let currentParent: any = [];
let previousParents: any[] = [];
let parseErrors: json.ParseError[] = [];
function onValue(value: any) {
if (Array.isArray(currentParent)) {
(<any[]>currentParent).push(value);
} else if (currentProperty) {
currentParent[currentProperty] = value;
}
}
let visitor: json.JSONVisitor = {
onObjectBegin: () => {
let object = {};
onValue(object);
previousParents.push(currentParent);
currentParent = object;
currentProperty = null;
},
onObjectProperty: (name: string) => {
currentProperty = name;
},
onObjectEnd: () => {
currentParent = previousParents.pop();
},
onArrayBegin: () => {
let array: any[] = [];
onValue(array);
previousParents.push(currentParent);
currentParent = array;
currentProperty = null;
},
onArrayEnd: () => {
currentParent = previousParents.pop();
},
onLiteralValue: onValue,
onError: (error: json.ParseErrorCode, offset: number, length: number) => {
parseErrors.push({ error, offset, length });
}
};
if (content) {
try {
json.visit(content, visitor);
raw = currentParent[0] || {};
} catch (e) {
console.error(`Error while parsing settings file ${this._name}: ${e}`);
this._parseErrors = [e];
}
}
return raw;
}
protected doParseRaw(raw: any): IConfigurationModel {
if (this._scopes) {
const configurationProperties = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties();
raw = this.filterByScope(raw, configurationProperties, true, this._scopes);
}
const contents = toValuesTree(raw, message => console.error(`Conflict in settings file ${this._name}: ${message}`));
const keys = Object.keys(raw);
const overrides: IOverrides[] = toOverrides(raw, message => console.error(`Conflict in settings file ${this._name}: ${message}`));
return { contents, keys, overrides };
}
private filterByScope(properties: any, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }, filterOverriddenProperties: boolean, scopes: ConfigurationScope[]): {} {
const result: any = {};
for (let key in properties) {
if (OVERRIDE_PROPERTY_PATTERN.test(key) && filterOverriddenProperties) {
result[key] = this.filterByScope(properties[key], configurationProperties, false, scopes);
} else {
const scope = this.getScope(key, configurationProperties);
// Load unregistered configurations always.
if (scope === undefined || scopes.indexOf(scope) !== -1) {
result[key] = properties[key];
}
}
}
return result;
}
private getScope(key: string, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }): ConfigurationScope | undefined {
const propertySchema = configurationProperties[key];
return propertySchema ? typeof propertySchema.scope !== 'undefined' ? propertySchema.scope : ConfigurationScope.WINDOW : undefined;
}
}
export class UserSettings extends Disposable {
private readonly parser: ConfigurationModelParser;
protected readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChange: Event<void> = this._onDidChange.event;
constructor(
private readonly userSettingsResource: URI,
private readonly scopes: ConfigurationScope[] | undefined,
private readonly fileService: IFileService
) {
super();
this.parser = new ConfigurationModelParser(this.userSettingsResource.toString(), this.scopes);
this._register(this.fileService.watch(dirname(this.userSettingsResource)));
this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.userSettingsResource))(() => this._onDidChange.fire()));
}
async loadConfiguration(): Promise<ConfigurationModel> {
try {
const content = await this.fileService.readFile(this.userSettingsResource);
this.parser.parseContent(content.value.toString() || '{}');
return this.parser.configurationModel;
} catch (e) {
return new ConfigurationModel();
}
}
reprocess(): ConfigurationModel {
this.parser.parse();
return this.parser.configurationModel;
}
}
export class Configuration {
private _workspaceConsolidatedConfiguration: ConfigurationModel | null = null;
private _foldersConsolidatedConfigurations: ResourceMap<ConfigurationModel> = new ResourceMap<ConfigurationModel>();
constructor(
private _defaultConfiguration: ConfigurationModel,
private _localUserConfiguration: ConfigurationModel,
private _remoteUserConfiguration: ConfigurationModel = new ConfigurationModel(),
private _workspaceConfiguration: ConfigurationModel = new ConfigurationModel(),
private _folderConfigurations: ResourceMap<ConfigurationModel> = new ResourceMap<ConfigurationModel>(),
private _memoryConfiguration: ConfigurationModel = new ConfigurationModel(),
private _memoryConfigurationByResource: ResourceMap<ConfigurationModel> = new ResourceMap<ConfigurationModel>(),
private _freeze: boolean = true) {
}
getValue(section: string | undefined, overrides: IConfigurationOverrides, workspace: Workspace | undefined): any {
const consolidateConfigurationModel = this.getConsolidateConfigurationModel(overrides, workspace);
return consolidateConfigurationModel.getValue(section);
}
updateValue(key: string, value: any, overrides: IConfigurationOverrides = {}): void {
let memoryConfiguration: ConfigurationModel | undefined;
if (overrides.resource) {
memoryConfiguration = this._memoryConfigurationByResource.get(overrides.resource);
if (!memoryConfiguration) {
memoryConfiguration = new ConfigurationModel();
this._memoryConfigurationByResource.set(overrides.resource, memoryConfiguration);
}
} else {
memoryConfiguration = this._memoryConfiguration;
}
if (value === undefined) {
memoryConfiguration.removeValue(key);
} else {
memoryConfiguration.setValue(key, value);
}
if (!overrides.resource) {
this._workspaceConsolidatedConfiguration = null;
}
}
inspect<C>(key: string, overrides: IConfigurationOverrides, workspace: Workspace | undefined): IConfigurationValue<C> {
const consolidateConfigurationModel = this.getConsolidateConfigurationModel(overrides, workspace);
const folderConfigurationModel = this.getFolderConfigurationModelForResource(overrides.resource, workspace);
const memoryConfigurationModel = overrides.resource ? this._memoryConfigurationByResource.get(overrides.resource) || this._memoryConfiguration : this._memoryConfiguration;
const defaultValue = overrides.overrideIdentifier ? this._defaultConfiguration.freeze().override(overrides.overrideIdentifier).getValue<C>(key) : this._defaultConfiguration.freeze().getValue<C>(key);
const userValue = overrides.overrideIdentifier ? this.userConfiguration.freeze().override(overrides.overrideIdentifier).getValue<C>(key) : this.userConfiguration.freeze().getValue<C>(key);
const userLocalValue = overrides.overrideIdentifier ? this.localUserConfiguration.freeze().override(overrides.overrideIdentifier).getValue<C>(key) : this.localUserConfiguration.freeze().getValue<C>(key);
const userRemoteValue = overrides.overrideIdentifier ? this.remoteUserConfiguration.freeze().override(overrides.overrideIdentifier).getValue<C>(key) : this.remoteUserConfiguration.freeze().getValue<C>(key);
const workspaceValue = workspace ? overrides.overrideIdentifier ? this._workspaceConfiguration.freeze().override(overrides.overrideIdentifier).getValue<C>(key) : this._workspaceConfiguration.freeze().getValue<C>(key) : undefined; //Check on workspace exists or not because _workspaceConfiguration is never null
const workspaceFolderValue = folderConfigurationModel ? overrides.overrideIdentifier ? folderConfigurationModel.freeze().override(overrides.overrideIdentifier).getValue<C>(key) : folderConfigurationModel.freeze().getValue<C>(key) : undefined;
const memoryValue = overrides.overrideIdentifier ? memoryConfigurationModel.override(overrides.overrideIdentifier).getValue<C>(key) : memoryConfigurationModel.getValue<C>(key);
const value = consolidateConfigurationModel.getValue<C>(key);
const overrideIdentifiers: string[] = arrays.distinct(arrays.flatten(consolidateConfigurationModel.overrides.map(override => override.identifiers))).filter(overrideIdentifier => consolidateConfigurationModel.getOverrideValue(key, overrideIdentifier) !== undefined);
return {
defaultValue: defaultValue,
userValue: userValue,
userLocalValue: userLocalValue,
userRemoteValue: userRemoteValue,
workspaceValue: workspaceValue,
workspaceFolderValue: workspaceFolderValue,
memoryValue: memoryValue,
value,
default: defaultValue !== undefined ? { value: this._defaultConfiguration.freeze().getValue(key), override: overrides.overrideIdentifier ? this._defaultConfiguration.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined,
user: userValue !== undefined ? { value: this.userConfiguration.freeze().getValue(key), override: overrides.overrideIdentifier ? this.userConfiguration.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined,
userLocal: userLocalValue !== undefined ? { value: this.localUserConfiguration.freeze().getValue(key), override: overrides.overrideIdentifier ? this.localUserConfiguration.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined,
userRemote: userRemoteValue !== undefined ? { value: this.remoteUserConfiguration.freeze().getValue(key), override: overrides.overrideIdentifier ? this.remoteUserConfiguration.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined,
workspace: workspaceValue !== undefined ? { value: this._workspaceConfiguration.freeze().getValue(key), override: overrides.overrideIdentifier ? this._workspaceConfiguration.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined,
workspaceFolder: workspaceFolderValue !== undefined ? { value: folderConfigurationModel?.freeze().getValue(key), override: overrides.overrideIdentifier ? folderConfigurationModel?.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined,
memory: memoryValue !== undefined ? { value: memoryConfigurationModel.getValue(key), override: overrides.overrideIdentifier ? memoryConfigurationModel.getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined,
overrideIdentifiers: overrideIdentifiers.length ? overrideIdentifiers : undefined
};
}
keys(workspace: Workspace | undefined): {
default: string[];
user: string[];
workspace: string[];
workspaceFolder: string[];
} {
const folderConfigurationModel = this.getFolderConfigurationModelForResource(undefined, workspace);
return {
default: this._defaultConfiguration.freeze().keys,
user: this.userConfiguration.freeze().keys,
workspace: this._workspaceConfiguration.freeze().keys,
workspaceFolder: folderConfigurationModel ? folderConfigurationModel.freeze().keys : []
};
}
updateDefaultConfiguration(defaultConfiguration: ConfigurationModel): void {
this._defaultConfiguration = defaultConfiguration;
this._workspaceConsolidatedConfiguration = null;
this._foldersConsolidatedConfigurations.clear();
}
updateLocalUserConfiguration(localUserConfiguration: ConfigurationModel): void {
this._localUserConfiguration = localUserConfiguration;
this._userConfiguration = null;
this._workspaceConsolidatedConfiguration = null;
this._foldersConsolidatedConfigurations.clear();
}
updateRemoteUserConfiguration(remoteUserConfiguration: ConfigurationModel): void {
this._remoteUserConfiguration = remoteUserConfiguration;
this._userConfiguration = null;
this._workspaceConsolidatedConfiguration = null;
this._foldersConsolidatedConfigurations.clear();
}
updateWorkspaceConfiguration(workspaceConfiguration: ConfigurationModel): void {
this._workspaceConfiguration = workspaceConfiguration;
this._workspaceConsolidatedConfiguration = null;
this._foldersConsolidatedConfigurations.clear();
}
updateFolderConfiguration(resource: URI, configuration: ConfigurationModel): void {
this._folderConfigurations.set(resource, configuration);
this._foldersConsolidatedConfigurations.delete(resource);
}
deleteFolderConfiguration(resource: URI): void {
this.folderConfigurations.delete(resource);
this._foldersConsolidatedConfigurations.delete(resource);
}
compareAndUpdateDefaultConfiguration(defaults: ConfigurationModel, keys: string[]): IConfigurationChange {
const overrides: [string, string[]][] = keys
.filter(key => OVERRIDE_PROPERTY_PATTERN.test(key))
.map(key => {
const overrideIdentifier = overrideIdentifierFromKey(key);
const fromKeys = this._defaultConfiguration.getKeysForOverrideIdentifier(overrideIdentifier);
const toKeys = defaults.getKeysForOverrideIdentifier(overrideIdentifier);
const keys = [
...toKeys.filter(key => fromKeys.indexOf(key) === -1),
...fromKeys.filter(key => toKeys.indexOf(key) === -1),
...fromKeys.filter(key => !objects.equals(this._defaultConfiguration.override(overrideIdentifier).getValue(key), defaults.override(overrideIdentifier).getValue(key)))
];
return [overrideIdentifier, keys];
});
this.updateDefaultConfiguration(defaults);
return { keys, overrides };
}
compareAndUpdateLocalUserConfiguration(user: ConfigurationModel): IConfigurationChange {
const { added, updated, removed, overrides } = compare(this.localUserConfiguration, user);
const keys = [...added, ...updated, ...removed];
if (keys.length) {
this.updateLocalUserConfiguration(user);
}
return { keys, overrides };
}
compareAndUpdateRemoteUserConfiguration(user: ConfigurationModel): IConfigurationChange {
const { added, updated, removed, overrides } = compare(this.remoteUserConfiguration, user);
let keys = [...added, ...updated, ...removed];
if (keys.length) {
this.updateRemoteUserConfiguration(user);
}
return { keys, overrides };
}
compareAndUpdateWorkspaceConfiguration(workspaceConfiguration: ConfigurationModel): IConfigurationChange {
const { added, updated, removed, overrides } = compare(this.workspaceConfiguration, workspaceConfiguration);
let keys = [...added, ...updated, ...removed];
if (keys.length) {
this.updateWorkspaceConfiguration(workspaceConfiguration);
}
return { keys, overrides };
}
compareAndUpdateFolderConfiguration(resource: URI, folderConfiguration: ConfigurationModel): IConfigurationChange {
const currentFolderConfiguration = this.folderConfigurations.get(resource);
const { added, updated, removed, overrides } = compare(currentFolderConfiguration, folderConfiguration);
let keys = [...added, ...updated, ...removed];
if (keys.length || !currentFolderConfiguration) {
this.updateFolderConfiguration(resource, folderConfiguration);
}
return { keys, overrides };
}
compareAndDeleteFolderConfiguration(folder: URI): IConfigurationChange {
const folderConfig = this.folderConfigurations.get(folder);
if (!folderConfig) {
throw new Error('Unknown folder');
}
this.deleteFolderConfiguration(folder);
const { added, updated, removed, overrides } = compare(folderConfig, undefined);
return { keys: [...added, ...updated, ...removed], overrides };
}
get defaults(): ConfigurationModel {
return this._defaultConfiguration;
}
private _userConfiguration: ConfigurationModel | null = null;
get userConfiguration(): ConfigurationModel {
if (!this._userConfiguration) {
this._userConfiguration = this._remoteUserConfiguration.isEmpty() ? this._localUserConfiguration : this._localUserConfiguration.merge(this._remoteUserConfiguration);
if (this._freeze) {
this._userConfiguration.freeze();
}
}
return this._userConfiguration;
}
get localUserConfiguration(): ConfigurationModel {
return this._localUserConfiguration;
}
get remoteUserConfiguration(): ConfigurationModel {
return this._remoteUserConfiguration;
}
get workspaceConfiguration(): ConfigurationModel {
return this._workspaceConfiguration;
}
protected get folderConfigurations(): ResourceMap<ConfigurationModel> {
return this._folderConfigurations;
}
private getConsolidateConfigurationModel(overrides: IConfigurationOverrides, workspace: Workspace | undefined): ConfigurationModel {
let configurationModel = this.getConsolidatedConfigurationModelForResource(overrides, workspace);
return overrides.overrideIdentifier ? configurationModel.override(overrides.overrideIdentifier) : configurationModel;
}
private getConsolidatedConfigurationModelForResource({ resource }: IConfigurationOverrides, workspace: Workspace | undefined): ConfigurationModel {
let consolidateConfiguration = this.getWorkspaceConsolidatedConfiguration();
if (workspace && resource) {
const root = workspace.getFolder(resource);
if (root) {
consolidateConfiguration = this.getFolderConsolidatedConfiguration(root.uri) || consolidateConfiguration;
}
const memoryConfigurationForResource = this._memoryConfigurationByResource.get(resource);
if (memoryConfigurationForResource) {
consolidateConfiguration = consolidateConfiguration.merge(memoryConfigurationForResource);
}
}
return consolidateConfiguration;
}
private getWorkspaceConsolidatedConfiguration(): ConfigurationModel {
if (!this._workspaceConsolidatedConfiguration) {
this._workspaceConsolidatedConfiguration = this._defaultConfiguration.merge(this.userConfiguration, this._workspaceConfiguration, this._memoryConfiguration);
if (this._freeze) {
this._workspaceConfiguration = this._workspaceConfiguration.freeze();
}
}
return this._workspaceConsolidatedConfiguration;
}
private getFolderConsolidatedConfiguration(folder: URI): ConfigurationModel {
let folderConsolidatedConfiguration = this._foldersConsolidatedConfigurations.get(folder);
if (!folderConsolidatedConfiguration) {
const workspaceConsolidateConfiguration = this.getWorkspaceConsolidatedConfiguration();
const folderConfiguration = this._folderConfigurations.get(folder);
if (folderConfiguration) {
folderConsolidatedConfiguration = workspaceConsolidateConfiguration.merge(folderConfiguration);
if (this._freeze) {
folderConsolidatedConfiguration = folderConsolidatedConfiguration.freeze();
}
this._foldersConsolidatedConfigurations.set(folder, folderConsolidatedConfiguration);
} else {
folderConsolidatedConfiguration = workspaceConsolidateConfiguration;
}
}
return folderConsolidatedConfiguration;
}
private getFolderConfigurationModelForResource(resource: URI | null | undefined, workspace: Workspace | undefined): ConfigurationModel | undefined {
if (workspace && resource) {
const root = workspace.getFolder(resource);
if (root) {
return this._folderConfigurations.get(root.uri);
}
}
return undefined;
}
toData(): IConfigurationData {
return {
defaults: {
contents: this._defaultConfiguration.contents,
overrides: this._defaultConfiguration.overrides,
keys: this._defaultConfiguration.keys
},
user: {
contents: this.userConfiguration.contents,
overrides: this.userConfiguration.overrides,
keys: this.userConfiguration.keys
},
workspace: {
contents: this._workspaceConfiguration.contents,
overrides: this._workspaceConfiguration.overrides,
keys: this._workspaceConfiguration.keys
},
folders: [...this._folderConfigurations.keys()].reduce<[UriComponents, IConfigurationModel][]>((result, folder) => {
const { contents, overrides, keys } = this._folderConfigurations.get(folder)!;
result.push([folder, { contents, overrides, keys }]);
return result;
}, [])
};
}
allKeys(): string[] {
const keys: Set<string> = new Set<string>();
this._defaultConfiguration.freeze().keys.forEach(key => keys.add(key));
this.userConfiguration.freeze().keys.forEach(key => keys.add(key));
this._workspaceConfiguration.freeze().keys.forEach(key => keys.add(key));
this._folderConfigurations.forEach(folderConfiguraiton => folderConfiguraiton.freeze().keys.forEach(key => keys.add(key)));
return [...keys.values()];
}
protected getAllKeysForOverrideIdentifier(overrideIdentifier: string): string[] {
const keys: Set<string> = new Set<string>();
this._defaultConfiguration.getKeysForOverrideIdentifier(overrideIdentifier).forEach(key => keys.add(key));
this.userConfiguration.getKeysForOverrideIdentifier(overrideIdentifier).forEach(key => keys.add(key));
this._workspaceConfiguration.getKeysForOverrideIdentifier(overrideIdentifier).forEach(key => keys.add(key));
this._folderConfigurations.forEach(folderConfiguraiton => folderConfiguraiton.getKeysForOverrideIdentifier(overrideIdentifier).forEach(key => keys.add(key)));
return [...keys.values()];
}
static parse(data: IConfigurationData): Configuration {
const defaultConfiguration = this.parseConfigurationModel(data.defaults);
const userConfiguration = this.parseConfigurationModel(data.user);
const workspaceConfiguration = this.parseConfigurationModel(data.workspace);
const folders: ResourceMap<ConfigurationModel> = data.folders.reduce((result, value) => {
result.set(URI.revive(value[0]), this.parseConfigurationModel(value[1]));
return result;
}, new ResourceMap<ConfigurationModel>());
return new Configuration(defaultConfiguration, userConfiguration, new ConfigurationModel(), workspaceConfiguration, folders, new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), false);
}
private static parseConfigurationModel(model: IConfigurationModel): ConfigurationModel {
return new ConfigurationModel(model.contents, model.keys, model.overrides).freeze();
}
}
export function mergeChanges(...changes: IConfigurationChange[]): IConfigurationChange {
if (changes.length === 0) {
return { keys: [], overrides: [] };
}
if (changes.length === 1) {
return changes[0];
}
const keysSet = new Set<string>();
const overridesMap = new Map<string, Set<string>>();
for (const change of changes) {
change.keys.forEach(key => keysSet.add(key));
change.overrides.forEach(([identifier, keys]) => {
const result = getOrSet(overridesMap, identifier, new Set<string>());
keys.forEach(key => result.add(key));
});
}
const overrides: [string, string[]][] = [];
overridesMap.forEach((keys, identifier) => overrides.push([identifier, [...keys.values()]]));
return { keys: [...keysSet.values()], overrides };
}
export class ConfigurationChangeEvent implements IConfigurationChangeEvent {
private readonly affectedKeysTree: any;
readonly affectedKeys: string[];
source!: ConfigurationTarget;
sourceConfig: any;
constructor(readonly change: IConfigurationChange, private readonly previous: { workspace?: Workspace, data: IConfigurationData } | undefined, private readonly currentConfiguraiton: Configuration, private readonly currentWorkspace?: Workspace) {
const keysSet = new Set<string>();
change.keys.forEach(key => keysSet.add(key));
change.overrides.forEach(([, keys]) => keys.forEach(key => keysSet.add(key)));
this.affectedKeys = [...keysSet.values()];
const configurationModel = new ConfigurationModel();
this.affectedKeys.forEach(key => configurationModel.setValue(key, {}));
this.affectedKeysTree = configurationModel.contents;
}
private _previousConfiguration: Configuration | undefined = undefined;
get previousConfiguration(): Configuration | undefined {
if (!this._previousConfiguration && this.previous) {
this._previousConfiguration = Configuration.parse(this.previous.data);
}
return this._previousConfiguration;
}
affectsConfiguration(section: string, overrides?: IConfigurationOverrides): boolean {
if (this.doesAffectedKeysTreeContains(this.affectedKeysTree, section)) {
if (overrides) {
const value1 = this.previousConfiguration ? this.previousConfiguration.getValue(section, overrides, this.previous?.workspace) : undefined;
const value2 = this.currentConfiguraiton.getValue(section, overrides, this.currentWorkspace);
return !objects.equals(value1, value2);
}
return true;
}
return false;
}
private doesAffectedKeysTreeContains(affectedKeysTree: any, section: string): boolean {
let requestedTree = toValuesTree({ [section]: true }, () => { });
let key;
while (typeof requestedTree === 'object' && (key = Object.keys(requestedTree)[0])) { // Only one key should present, since we added only one property
affectedKeysTree = affectedKeysTree[key];
if (!affectedKeysTree) {
return false; // Requested tree is not found
}
requestedTree = requestedTree[key];
}
return true;
}
}
export class AllKeysConfigurationChangeEvent extends ConfigurationChangeEvent {
constructor(configuration: Configuration, workspace: Workspace, public source: ConfigurationTarget, public sourceConfig: any) {
super({ keys: configuration.allKeys(), overrides: [] }, undefined, configuration, workspace);
}
}

View File

@@ -0,0 +1,499 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { Event, Emitter } from 'vs/base/common/event';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { Registry } from 'vs/platform/registry/common/platform';
import * as types from 'vs/base/common/types';
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import { IStringDictionary } from 'vs/base/common/collections';
export const Extensions = {
Configuration: 'base.contributions.configuration'
};
export interface IConfigurationRegistry {
/**
* Register a configuration to the registry.
*/
registerConfiguration(configuration: IConfigurationNode): void;
/**
* Register multiple configurations to the registry.
*/
registerConfigurations(configurations: IConfigurationNode[], validate?: boolean): void;
/**
* Deregister multiple configurations from the registry.
*/
deregisterConfigurations(configurations: IConfigurationNode[]): void;
/**
* Register multiple default configurations to the registry.
*/
registerDefaultConfigurations(defaultConfigurations: IStringDictionary<any>[]): void;
/**
* Deregister multiple default configurations from the registry.
*/
deregisterDefaultConfigurations(defaultConfigurations: IStringDictionary<any>[]): void;
/**
* Signal that the schema of a configuration setting has changes. It is currently only supported to change enumeration values.
* Property or default value changes are not allowed.
*/
notifyConfigurationSchemaUpdated(...configurations: IConfigurationNode[]): void;
/**
* Event that fires whenver a configuration has been
* registered.
*/
onDidSchemaChange: Event<void>;
/**
* Event that fires whenver a configuration has been
* registered.
*/
onDidUpdateConfiguration: Event<string[]>;
/**
* Returns all configuration nodes contributed to this registry.
*/
getConfigurations(): IConfigurationNode[];
/**
* Returns all configurations settings of all configuration nodes contributed to this registry.
*/
getConfigurationProperties(): { [qualifiedKey: string]: IConfigurationPropertySchema };
/**
* Returns all excluded configurations settings of all configuration nodes contributed to this registry.
*/
getExcludedConfigurationProperties(): { [qualifiedKey: string]: IConfigurationPropertySchema };
/**
* Register the identifiers for editor configurations
*/
registerOverrideIdentifiers(identifiers: string[]): void;
}
export const enum ConfigurationScope {
/**
* Application specific configuration, which can be configured only in local user settings.
*/
APPLICATION = 1,
/**
* Machine specific configuration, which can be configured only in local and remote user settings.
*/
MACHINE,
/**
* Window specific configuration, which can be configured in the user or workspace settings.
*/
WINDOW,
/**
* Resource specific configuration, which can be configured in the user, workspace or folder settings.
*/
RESOURCE,
/**
* Resource specific configuration that can be configured in language specific settings
*/
LANGUAGE_OVERRIDABLE,
/**
* Machine specific configuration that can also be configured in workspace or folder settings.
*/
MACHINE_OVERRIDABLE,
}
export interface IConfigurationPropertySchema extends IJSONSchema {
scope?: ConfigurationScope;
included?: boolean;
tags?: string[];
disallowSyncIgnore?: boolean;
enumItemLabels?: string[];
}
export interface IConfigurationExtensionInfo {
id: string;
}
export interface IConfigurationNode {
id?: string;
order?: number;
type?: string | string[];
title?: string;
description?: string;
properties?: { [path: string]: IConfigurationPropertySchema; };
allOf?: IConfigurationNode[];
scope?: ConfigurationScope;
extensionInfo?: IConfigurationExtensionInfo;
}
type SettingProperties = { [key: string]: any };
export const allSettings: { properties: SettingProperties, patternProperties: SettingProperties } = { properties: {}, patternProperties: {} };
export const applicationSettings: { properties: SettingProperties, patternProperties: SettingProperties } = { properties: {}, patternProperties: {} };
export const machineSettings: { properties: SettingProperties, patternProperties: SettingProperties } = { properties: {}, patternProperties: {} };
export const machineOverridableSettings: { properties: SettingProperties, patternProperties: SettingProperties } = { properties: {}, patternProperties: {} };
export const windowSettings: { properties: SettingProperties, patternProperties: SettingProperties } = { properties: {}, patternProperties: {} };
export const resourceSettings: { properties: SettingProperties, patternProperties: SettingProperties } = { properties: {}, patternProperties: {} };
export const resourceLanguageSettingsSchemaId = 'vscode://schemas/settings/resourceLanguage';
const contributionRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
class ConfigurationRegistry implements IConfigurationRegistry {
private readonly defaultValues: IStringDictionary<any>;
private readonly defaultLanguageConfigurationOverridesNode: IConfigurationNode;
private readonly configurationContributors: IConfigurationNode[];
private readonly configurationProperties: { [qualifiedKey: string]: IJSONSchema };
private readonly excludedConfigurationProperties: { [qualifiedKey: string]: IJSONSchema };
private readonly resourceLanguageSettingsSchema: IJSONSchema;
private readonly overrideIdentifiers = new Set<string>();
private readonly _onDidSchemaChange = new Emitter<void>();
readonly onDidSchemaChange: Event<void> = this._onDidSchemaChange.event;
private readonly _onDidUpdateConfiguration: Emitter<string[]> = new Emitter<string[]>();
readonly onDidUpdateConfiguration: Event<string[]> = this._onDidUpdateConfiguration.event;
constructor() {
this.defaultValues = {};
this.defaultLanguageConfigurationOverridesNode = {
id: 'defaultOverrides',
title: nls.localize('defaultLanguageConfigurationOverrides.title', "Default Language Configuration Overrides"),
properties: {}
};
this.configurationContributors = [this.defaultLanguageConfigurationOverridesNode];
this.resourceLanguageSettingsSchema = { properties: {}, patternProperties: {}, additionalProperties: false, errorMessage: 'Unknown editor configuration setting', allowTrailingCommas: true, allowComments: true };
this.configurationProperties = {};
this.excludedConfigurationProperties = {};
contributionRegistry.registerSchema(resourceLanguageSettingsSchemaId, this.resourceLanguageSettingsSchema);
}
public registerConfiguration(configuration: IConfigurationNode, validate: boolean = true): void {
this.registerConfigurations([configuration], validate);
}
public registerConfigurations(configurations: IConfigurationNode[], validate: boolean = true): void {
const properties: string[] = [];
configurations.forEach(configuration => {
properties.push(...this.validateAndRegisterProperties(configuration, validate)); // fills in defaults
this.configurationContributors.push(configuration);
this.registerJSONConfiguration(configuration);
});
contributionRegistry.registerSchema(resourceLanguageSettingsSchemaId, this.resourceLanguageSettingsSchema);
this._onDidSchemaChange.fire();
this._onDidUpdateConfiguration.fire(properties);
}
public deregisterConfigurations(configurations: IConfigurationNode[]): void {
const properties: string[] = [];
const deregisterConfiguration = (configuration: IConfigurationNode) => {
if (configuration.properties) {
for (const key in configuration.properties) {
properties.push(key);
delete this.configurationProperties[key];
this.removeFromSchema(key, configuration.properties[key]);
}
}
if (configuration.allOf) {
configuration.allOf.forEach(node => deregisterConfiguration(node));
}
};
for (const configuration of configurations) {
deregisterConfiguration(configuration);
const index = this.configurationContributors.indexOf(configuration);
if (index !== -1) {
this.configurationContributors.splice(index, 1);
}
}
contributionRegistry.registerSchema(resourceLanguageSettingsSchemaId, this.resourceLanguageSettingsSchema);
this._onDidSchemaChange.fire();
this._onDidUpdateConfiguration.fire(properties);
}
public registerDefaultConfigurations(defaultConfigurations: IStringDictionary<any>[]): void {
const properties: string[] = [];
const overrideIdentifiers: string[] = [];
for (const defaultConfiguration of defaultConfigurations) {
for (const key in defaultConfiguration) {
properties.push(key);
if (OVERRIDE_PROPERTY_PATTERN.test(key)) {
this.defaultValues[key] = { ...(this.defaultValues[key] || {}), ...defaultConfiguration[key] };
const property: IConfigurationPropertySchema = {
type: 'object',
default: this.defaultValues[key],
description: nls.localize('defaultLanguageConfiguration.description', "Configure settings to be overridden for {0} language.", key),
$ref: resourceLanguageSettingsSchemaId
};
overrideIdentifiers.push(overrideIdentifierFromKey(key));
this.configurationProperties[key] = property;
this.defaultLanguageConfigurationOverridesNode.properties![key] = property;
} else {
this.defaultValues[key] = defaultConfiguration[key];
const property = this.configurationProperties[key];
if (property) {
this.updatePropertyDefaultValue(key, property);
this.updateSchema(key, property);
}
}
}
}
this.registerOverrideIdentifiers(overrideIdentifiers);
this._onDidSchemaChange.fire();
this._onDidUpdateConfiguration.fire(properties);
}
public deregisterDefaultConfigurations(defaultConfigurations: IStringDictionary<any>[]): void {
const properties: string[] = [];
for (const defaultConfiguration of defaultConfigurations) {
for (const key in defaultConfiguration) {
properties.push(key);
delete this.defaultValues[key];
if (OVERRIDE_PROPERTY_PATTERN.test(key)) {
delete this.configurationProperties[key];
delete this.defaultLanguageConfigurationOverridesNode.properties![key];
} else {
const property = this.configurationProperties[key];
if (property) {
this.updatePropertyDefaultValue(key, property);
this.updateSchema(key, property);
}
}
}
}
this.updateOverridePropertyPatternKey();
this._onDidSchemaChange.fire();
this._onDidUpdateConfiguration.fire(properties);
}
public notifyConfigurationSchemaUpdated(...configurations: IConfigurationNode[]) {
this._onDidSchemaChange.fire();
}
public registerOverrideIdentifiers(overrideIdentifiers: string[]): void {
for (const overrideIdentifier of overrideIdentifiers) {
this.overrideIdentifiers.add(overrideIdentifier);
}
this.updateOverridePropertyPatternKey();
}
private validateAndRegisterProperties(configuration: IConfigurationNode, validate: boolean = true, scope: ConfigurationScope = ConfigurationScope.WINDOW): string[] {
scope = types.isUndefinedOrNull(configuration.scope) ? scope : configuration.scope;
let propertyKeys: string[] = [];
let properties = configuration.properties;
if (properties) {
for (let key in properties) {
if (validate && validateProperty(key)) {
delete properties[key];
continue;
}
const property = properties[key];
// update default value
this.updatePropertyDefaultValue(key, property);
// update scope
if (OVERRIDE_PROPERTY_PATTERN.test(key)) {
property.scope = undefined; // No scope for overridable properties `[${identifier}]`
} else {
property.scope = types.isUndefinedOrNull(property.scope) ? scope : property.scope;
}
// Add to properties maps
// Property is included by default if 'included' is unspecified
if (properties[key].hasOwnProperty('included') && !properties[key].included) {
this.excludedConfigurationProperties[key] = properties[key];
delete properties[key];
continue;
} else {
this.configurationProperties[key] = properties[key];
}
if (!properties[key].deprecationMessage && properties[key].markdownDeprecationMessage) {
// If not set, default deprecationMessage to the markdown source
properties[key].deprecationMessage = properties[key].markdownDeprecationMessage;
}
propertyKeys.push(key);
}
}
let subNodes = configuration.allOf;
if (subNodes) {
for (let node of subNodes) {
propertyKeys.push(...this.validateAndRegisterProperties(node, validate, scope));
}
}
return propertyKeys;
}
getConfigurations(): IConfigurationNode[] {
return this.configurationContributors;
}
getConfigurationProperties(): { [qualifiedKey: string]: IConfigurationPropertySchema } {
return this.configurationProperties;
}
getExcludedConfigurationProperties(): { [qualifiedKey: string]: IConfigurationPropertySchema } {
return this.excludedConfigurationProperties;
}
private registerJSONConfiguration(configuration: IConfigurationNode) {
const register = (configuration: IConfigurationNode) => {
let properties = configuration.properties;
if (properties) {
for (const key in properties) {
this.updateSchema(key, properties[key]);
}
}
let subNodes = configuration.allOf;
if (subNodes) {
subNodes.forEach(register);
}
};
register(configuration);
}
private updateSchema(key: string, property: IConfigurationPropertySchema): void {
allSettings.properties[key] = property;
switch (property.scope) {
case ConfigurationScope.APPLICATION:
applicationSettings.properties[key] = property;
break;
case ConfigurationScope.MACHINE:
machineSettings.properties[key] = property;
break;
case ConfigurationScope.MACHINE_OVERRIDABLE:
machineOverridableSettings.properties[key] = property;
break;
case ConfigurationScope.WINDOW:
windowSettings.properties[key] = property;
break;
case ConfigurationScope.RESOURCE:
resourceSettings.properties[key] = property;
break;
case ConfigurationScope.LANGUAGE_OVERRIDABLE:
resourceSettings.properties[key] = property;
this.resourceLanguageSettingsSchema.properties![key] = property;
break;
}
}
private removeFromSchema(key: string, property: IConfigurationPropertySchema): void {
delete allSettings.properties[key];
switch (property.scope) {
case ConfigurationScope.APPLICATION:
delete applicationSettings.properties[key];
break;
case ConfigurationScope.MACHINE:
delete machineSettings.properties[key];
break;
case ConfigurationScope.MACHINE_OVERRIDABLE:
delete machineOverridableSettings.properties[key];
break;
case ConfigurationScope.WINDOW:
delete windowSettings.properties[key];
break;
case ConfigurationScope.RESOURCE:
case ConfigurationScope.LANGUAGE_OVERRIDABLE:
delete resourceSettings.properties[key];
break;
}
}
private updateOverridePropertyPatternKey(): void {
for (const overrideIdentifier of this.overrideIdentifiers.values()) {
const overrideIdentifierProperty = `[${overrideIdentifier}]`;
const resourceLanguagePropertiesSchema: IJSONSchema = {
type: 'object',
description: nls.localize('overrideSettings.defaultDescription', "Configure editor settings to be overridden for a language."),
errorMessage: nls.localize('overrideSettings.errorMessage', "This setting does not support per-language configuration."),
$ref: resourceLanguageSettingsSchemaId,
};
this.updatePropertyDefaultValue(overrideIdentifierProperty, resourceLanguagePropertiesSchema);
allSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema;
applicationSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema;
machineSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema;
machineOverridableSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema;
windowSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema;
resourceSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema;
}
this._onDidSchemaChange.fire();
}
private updatePropertyDefaultValue(key: string, property: IConfigurationPropertySchema): void {
let defaultValue = this.defaultValues[key];
if (types.isUndefined(defaultValue)) {
defaultValue = property.default;
}
if (types.isUndefined(defaultValue)) {
defaultValue = getDefaultValue(property.type);
}
property.default = defaultValue;
}
}
const OVERRIDE_PROPERTY = '\\[.*\\]$';
export const OVERRIDE_PROPERTY_PATTERN = new RegExp(OVERRIDE_PROPERTY);
export function overrideIdentifierFromKey(key: string): string {
return key.substring(1, key.length - 1);
}
export function getDefaultValue(type: string | string[] | undefined): any {
const t = Array.isArray(type) ? (<string[]>type)[0] : <string>type;
switch (t) {
case 'boolean':
return false;
case 'integer':
case 'number':
return 0;
case 'string':
return '';
case 'array':
return [];
case 'object':
return {};
default:
return null;
}
}
const configurationRegistry = new ConfigurationRegistry();
Registry.add(Extensions.Configuration, configurationRegistry);
export function validateProperty(property: string): string | null {
if (OVERRIDE_PROPERTY_PATTERN.test(property)) {
return nls.localize('config.property.languageDefault', "Cannot register '{0}'. This matches property pattern '\\\\[.*\\\\]$' for describing language specific editor settings. Use 'configurationDefaults' contribution.", property);
}
if (configurationRegistry.getConfigurationProperties()[property] !== undefined) {
return nls.localize('config.property.duplicate', "Cannot register '{0}'. This property is already registered.", property);
}
return null;
}
export function getScopes(): [string, ConfigurationScope | undefined][] {
const scopes: [string, ConfigurationScope | undefined][] = [];
const configurationProperties = configurationRegistry.getConfigurationProperties();
for (const key of Object.keys(configurationProperties)) {
scopes.push([key, configurationProperties[key].scope]);
}
scopes.push(['launch', ConfigurationScope.RESOURCE]);
scopes.push(['task', ConfigurationScope.RESOURCE]);
return scopes;
}

View File

@@ -0,0 +1,113 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Registry } from 'vs/platform/registry/common/platform';
import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { IConfigurationService, IConfigurationChangeEvent, IConfigurationOverrides, ConfigurationTarget, isConfigurationOverrides, IConfigurationData, IConfigurationValue, IConfigurationChange } from 'vs/platform/configuration/common/configuration';
import { DefaultConfigurationModel, Configuration, ConfigurationModel, ConfigurationChangeEvent, UserSettings } from 'vs/platform/configuration/common/configurationModels';
import { Event, Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
import { RunOnceScheduler } from 'vs/base/common/async';
export class ConfigurationService extends Disposable implements IConfigurationService, IDisposable {
declare readonly _serviceBrand: undefined;
private configuration: Configuration;
private userConfiguration: UserSettings;
private readonly reloadConfigurationScheduler: RunOnceScheduler;
private readonly _onDidChangeConfiguration: Emitter<IConfigurationChangeEvent> = this._register(new Emitter<IConfigurationChangeEvent>());
readonly onDidChangeConfiguration: Event<IConfigurationChangeEvent> = this._onDidChangeConfiguration.event;
constructor(
private readonly settingsResource: URI,
fileService: IFileService
) {
super();
this.userConfiguration = this._register(new UserSettings(this.settingsResource, undefined, fileService));
this.configuration = new Configuration(new DefaultConfigurationModel(), new ConfigurationModel());
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reloadConfiguration(), 50));
this._register(Registry.as<IConfigurationRegistry>(Extensions.Configuration).onDidUpdateConfiguration(configurationProperties => this.onDidDefaultConfigurationChange(configurationProperties)));
this._register(this.userConfiguration.onDidChange(() => this.reloadConfigurationScheduler.schedule()));
}
async initialize(): Promise<void> {
const userConfiguration = await this.userConfiguration.loadConfiguration();
this.configuration = new Configuration(new DefaultConfigurationModel(), userConfiguration);
}
getConfigurationData(): IConfigurationData {
return this.configuration.toData();
}
getValue<T>(): T;
getValue<T>(section: string): T;
getValue<T>(overrides: IConfigurationOverrides): T;
getValue<T>(section: string, overrides: IConfigurationOverrides): T;
getValue(arg1?: any, arg2?: any): any {
const section = typeof arg1 === 'string' ? arg1 : undefined;
const overrides = isConfigurationOverrides(arg1) ? arg1 : isConfigurationOverrides(arg2) ? arg2 : {};
return this.configuration.getValue(section, overrides, undefined);
}
updateValue(key: string, value: any): Promise<void>;
updateValue(key: string, value: any, overrides: IConfigurationOverrides): Promise<void>;
updateValue(key: string, value: any, target: ConfigurationTarget): Promise<void>;
updateValue(key: string, value: any, overrides: IConfigurationOverrides, target: ConfigurationTarget): Promise<void>;
updateValue(key: string, value: any, arg3?: any, arg4?: any): Promise<void> {
return Promise.reject(new Error('not supported'));
}
inspect<T>(key: string): IConfigurationValue<T> {
return this.configuration.inspect<T>(key, {}, undefined);
}
keys(): {
default: string[];
user: string[];
workspace: string[];
workspaceFolder: string[];
} {
return this.configuration.keys(undefined);
}
async reloadConfiguration(): Promise<void> {
const configurationModel = await this.userConfiguration.loadConfiguration();
this.onDidChangeUserConfiguration(configurationModel);
}
private onDidChangeUserConfiguration(userConfigurationModel: ConfigurationModel): void {
const previous = this.configuration.toData();
const change = this.configuration.compareAndUpdateLocalUserConfiguration(userConfigurationModel);
this.trigger(change, previous, ConfigurationTarget.USER);
}
private onDidDefaultConfigurationChange(keys: string[]): void {
const previous = this.configuration.toData();
const change = this.configuration.compareAndUpdateDefaultConfiguration(new DefaultConfigurationModel(), keys);
this.trigger(change, previous, ConfigurationTarget.DEFAULT);
}
private trigger(configurationChange: IConfigurationChange, previous: IConfigurationData, source: ConfigurationTarget): void {
const event = new ConfigurationChangeEvent(configurationChange, { data: previous }, this.configuration);
event.source = source;
event.sourceConfig = this.getTargetConfiguration(source);
this._onDidChangeConfiguration.fire(event);
}
private getTargetConfiguration(target: ConfigurationTarget): any {
switch (target) {
case ConfigurationTarget.DEFAULT:
return this.configuration.defaults.contents;
case ConfigurationTarget.USER:
return this.configuration.localUserConfiguration.contents;
}
return {};
}
}

View File

@@ -0,0 +1,147 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { merge, removeFromValueTree } from 'vs/platform/configuration/common/configuration';
import { mergeChanges } from 'vs/platform/configuration/common/configurationModels';
suite('Configuration', () => {
test('simple merge', () => {
let base = { 'a': 1, 'b': 2 };
merge(base, { 'a': 3, 'c': 4 }, true);
assert.deepEqual(base, { 'a': 3, 'b': 2, 'c': 4 });
base = { 'a': 1, 'b': 2 };
merge(base, { 'a': 3, 'c': 4 }, false);
assert.deepEqual(base, { 'a': 1, 'b': 2, 'c': 4 });
});
test('removeFromValueTree: remove a non existing key', () => {
let target = { 'a': { 'b': 2 } };
removeFromValueTree(target, 'c');
assert.deepEqual(target, { 'a': { 'b': 2 } });
});
test('removeFromValueTree: remove a multi segmented key from an object that has only sub sections of the key', () => {
let target = { 'a': { 'b': 2 } };
removeFromValueTree(target, 'a.b.c');
assert.deepEqual(target, { 'a': { 'b': 2 } });
});
test('removeFromValueTree: remove a single segmented key', () => {
let target = { 'a': 1 };
removeFromValueTree(target, 'a');
assert.deepEqual(target, {});
});
test('removeFromValueTree: remove a single segmented key when its value is undefined', () => {
let target = { 'a': undefined };
removeFromValueTree(target, 'a');
assert.deepEqual(target, {});
});
test('removeFromValueTree: remove a multi segmented key when its value is undefined', () => {
let target = { 'a': { 'b': 1 } };
removeFromValueTree(target, 'a.b');
assert.deepEqual(target, {});
});
test('removeFromValueTree: remove a multi segmented key when its value is array', () => {
let target = { 'a': { 'b': [1] } };
removeFromValueTree(target, 'a.b');
assert.deepEqual(target, {});
});
test('removeFromValueTree: remove a multi segmented key first segment value is array', () => {
let target = { 'a': [1] };
removeFromValueTree(target, 'a.0');
assert.deepEqual(target, { 'a': [1] });
});
test('removeFromValueTree: remove when key is the first segmenet', () => {
let target = { 'a': { 'b': 1 } };
removeFromValueTree(target, 'a');
assert.deepEqual(target, {});
});
test('removeFromValueTree: remove a multi segmented key when the first node has more values', () => {
let target = { 'a': { 'b': { 'c': 1 }, 'd': 1 } };
removeFromValueTree(target, 'a.b.c');
assert.deepEqual(target, { 'a': { 'd': 1 } });
});
test('removeFromValueTree: remove a multi segmented key when in between node has more values', () => {
let target = { 'a': { 'b': { 'c': { 'd': 1 }, 'd': 1 } } };
removeFromValueTree(target, 'a.b.c.d');
assert.deepEqual(target, { 'a': { 'b': { 'd': 1 } } });
});
test('removeFromValueTree: remove a multi segmented key when the last but one node has more values', () => {
let target = { 'a': { 'b': { 'c': 1, 'd': 1 } } };
removeFromValueTree(target, 'a.b.c');
assert.deepEqual(target, { 'a': { 'b': { 'd': 1 } } });
});
});
suite('Configuration Changes: Merge', () => {
test('merge only keys', () => {
const actual = mergeChanges({ keys: ['a', 'b'], overrides: [] }, { keys: ['c', 'd'], overrides: [] });
assert.deepEqual(actual, { keys: ['a', 'b', 'c', 'd'], overrides: [] });
});
test('merge only keys with duplicates', () => {
const actual = mergeChanges({ keys: ['a', 'b'], overrides: [] }, { keys: ['c', 'd'], overrides: [] }, { keys: ['a', 'd', 'e'], overrides: [] });
assert.deepEqual(actual, { keys: ['a', 'b', 'c', 'd', 'e'], overrides: [] });
});
test('merge only overrides', () => {
const actual = mergeChanges({ keys: [], overrides: [['a', ['1', '2']]] }, { keys: [], overrides: [['b', ['3', '4']]] });
assert.deepEqual(actual, { keys: [], overrides: [['a', ['1', '2']], ['b', ['3', '4']]] });
});
test('merge only overrides with duplicates', () => {
const actual = mergeChanges({ keys: [], overrides: [['a', ['1', '2']], ['b', ['5', '4']]] }, { keys: [], overrides: [['b', ['3', '4']]] }, { keys: [], overrides: [['c', ['1', '4']], ['a', ['2', '3']]] });
assert.deepEqual(actual, { keys: [], overrides: [['a', ['1', '2', '3']], ['b', ['5', '4', '3']], ['c', ['1', '4']]] });
});
test('merge', () => {
const actual = mergeChanges({ keys: ['b', 'b'], overrides: [['a', ['1', '2']], ['b', ['5', '4']]] }, { keys: ['b'], overrides: [['b', ['3', '4']]] }, { keys: ['c', 'a'], overrides: [['c', ['1', '4']], ['a', ['2', '3']]] });
assert.deepEqual(actual, { keys: ['b', 'c', 'a'], overrides: [['a', ['1', '2', '3']], ['b', ['5', '4', '3']], ['c', ['1', '4']]] });
});
test('merge single change', () => {
const actual = mergeChanges({ keys: ['b', 'b'], overrides: [['a', ['1', '2']], ['b', ['5', '4']]] });
assert.deepEqual(actual, { keys: ['b', 'b'], overrides: [['a', ['1', '2']], ['b', ['5', '4']]] });
});
test('merge no changes', () => {
const actual = mergeChanges();
assert.deepEqual(actual, { keys: [], overrides: [] });
});
});

View File

@@ -0,0 +1,950 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { ConfigurationModel, DefaultConfigurationModel, ConfigurationChangeEvent, ConfigurationModelParser, Configuration, mergeChanges, AllKeysConfigurationChangeEvent } from 'vs/platform/configuration/common/configurationModels';
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { URI } from 'vs/base/common/uri';
import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { join } from 'vs/base/common/path';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
suite('ConfigurationModel', () => {
test('setValue for a key that has no sections and not defined', () => {
let testObject = new ConfigurationModel({ 'a': { 'b': 1 } }, ['a.b']);
testObject.setValue('f', 1);
assert.deepEqual(testObject.contents, { 'a': { 'b': 1 }, 'f': 1 });
assert.deepEqual(testObject.keys, ['a.b', 'f']);
});
test('setValue for a key that has no sections and defined', () => {
let testObject = new ConfigurationModel({ 'a': { 'b': 1 }, 'f': 1 }, ['a.b', 'f']);
testObject.setValue('f', 3);
assert.deepEqual(testObject.contents, { 'a': { 'b': 1 }, 'f': 3 });
assert.deepEqual(testObject.keys, ['a.b', 'f']);
});
test('setValue for a key that has sections and not defined', () => {
let testObject = new ConfigurationModel({ 'a': { 'b': 1 }, 'f': 1 }, ['a.b', 'f']);
testObject.setValue('b.c', 1);
assert.deepEqual(testObject.contents, { 'a': { 'b': 1 }, 'b': { 'c': 1 }, 'f': 1 });
assert.deepEqual(testObject.keys, ['a.b', 'f', 'b.c']);
});
test('setValue for a key that has sections and defined', () => {
let testObject = new ConfigurationModel({ 'a': { 'b': 1 }, 'b': { 'c': 1 }, 'f': 1 }, ['a.b', 'b.c', 'f']);
testObject.setValue('b.c', 3);
assert.deepEqual(testObject.contents, { 'a': { 'b': 1 }, 'b': { 'c': 3 }, 'f': 1 });
assert.deepEqual(testObject.keys, ['a.b', 'b.c', 'f']);
});
test('setValue for a key that has sections and sub section not defined', () => {
let testObject = new ConfigurationModel({ 'a': { 'b': 1 }, 'f': 1 }, ['a.b', 'f']);
testObject.setValue('a.c', 1);
assert.deepEqual(testObject.contents, { 'a': { 'b': 1, 'c': 1 }, 'f': 1 });
assert.deepEqual(testObject.keys, ['a.b', 'f', 'a.c']);
});
test('setValue for a key that has sections and sub section defined', () => {
let testObject = new ConfigurationModel({ 'a': { 'b': 1, 'c': 1 }, 'f': 1 }, ['a.b', 'a.c', 'f']);
testObject.setValue('a.c', 3);
assert.deepEqual(testObject.contents, { 'a': { 'b': 1, 'c': 3 }, 'f': 1 });
assert.deepEqual(testObject.keys, ['a.b', 'a.c', 'f']);
});
test('setValue for a key that has sections and last section is added', () => {
let testObject = new ConfigurationModel({ 'a': { 'b': {} }, 'f': 1 }, ['a.b', 'f']);
testObject.setValue('a.b.c', 1);
assert.deepEqual(testObject.contents, { 'a': { 'b': { 'c': 1 } }, 'f': 1 });
assert.deepEqual(testObject.keys, ['a.b.c', 'f']);
});
test('removeValue: remove a non existing key', () => {
let testObject = new ConfigurationModel({ 'a': { 'b': 2 } }, ['a.b']);
testObject.removeValue('a.b.c');
assert.deepEqual(testObject.contents, { 'a': { 'b': 2 } });
assert.deepEqual(testObject.keys, ['a.b']);
});
test('removeValue: remove a single segmented key', () => {
let testObject = new ConfigurationModel({ 'a': 1 }, ['a']);
testObject.removeValue('a');
assert.deepEqual(testObject.contents, {});
assert.deepEqual(testObject.keys, []);
});
test('removeValue: remove a multi segmented key', () => {
let testObject = new ConfigurationModel({ 'a': { 'b': 1 } }, ['a.b']);
testObject.removeValue('a.b');
assert.deepEqual(testObject.contents, {});
assert.deepEqual(testObject.keys, []);
});
test('get overriding configuration model for an existing identifier', () => {
let testObject = new ConfigurationModel(
{ 'a': { 'b': 1 }, 'f': 1 }, [],
[{ identifiers: ['c'], contents: { 'a': { 'd': 1 } }, keys: ['a'] }]);
assert.deepEqual(testObject.override('c').contents, { 'a': { 'b': 1, 'd': 1 }, 'f': 1 });
});
test('get overriding configuration model for an identifier that does not exist', () => {
let testObject = new ConfigurationModel(
{ 'a': { 'b': 1 }, 'f': 1 }, [],
[{ identifiers: ['c'], contents: { 'a': { 'd': 1 } }, keys: ['a'] }]);
assert.deepEqual(testObject.override('xyz').contents, { 'a': { 'b': 1 }, 'f': 1 });
});
test('get overriding configuration when one of the keys does not exist in base', () => {
let testObject = new ConfigurationModel(
{ 'a': { 'b': 1 }, 'f': 1 }, [],
[{ identifiers: ['c'], contents: { 'a': { 'd': 1 }, 'g': 1 }, keys: ['a', 'g'] }]);
assert.deepEqual(testObject.override('c').contents, { 'a': { 'b': 1, 'd': 1 }, 'f': 1, 'g': 1 });
});
test('get overriding configuration when one of the key in base is not of object type', () => {
let testObject = new ConfigurationModel(
{ 'a': { 'b': 1 }, 'f': 1 }, [],
[{ identifiers: ['c'], contents: { 'a': { 'd': 1 }, 'f': { 'g': 1 } }, keys: ['a', 'f'] }]);
assert.deepEqual(testObject.override('c').contents, { 'a': { 'b': 1, 'd': 1 }, 'f': { 'g': 1 } });
});
test('get overriding configuration when one of the key in overriding contents is not of object type', () => {
let testObject = new ConfigurationModel(
{ 'a': { 'b': 1 }, 'f': { 'g': 1 } }, [],
[{ identifiers: ['c'], contents: { 'a': { 'd': 1 }, 'f': 1 }, keys: ['a', 'f'] }]);
assert.deepEqual(testObject.override('c').contents, { 'a': { 'b': 1, 'd': 1 }, 'f': 1 });
});
test('get overriding configuration if the value of overriding identifier is not object', () => {
let testObject = new ConfigurationModel(
{ 'a': { 'b': 1 }, 'f': { 'g': 1 } }, [],
[{ identifiers: ['c'], contents: 'abc', keys: [] }]);
assert.deepEqual(testObject.override('c').contents, { 'a': { 'b': 1 }, 'f': { 'g': 1 } });
});
test('get overriding configuration if the value of overriding identifier is an empty object', () => {
let testObject = new ConfigurationModel(
{ 'a': { 'b': 1 }, 'f': { 'g': 1 } }, [],
[{ identifiers: ['c'], contents: {}, keys: [] }]);
assert.deepEqual(testObject.override('c').contents, { 'a': { 'b': 1 }, 'f': { 'g': 1 } });
});
test('simple merge', () => {
let base = new ConfigurationModel({ 'a': 1, 'b': 2 }, ['a', 'b']);
let add = new ConfigurationModel({ 'a': 3, 'c': 4 }, ['a', 'c']);
let result = base.merge(add);
assert.deepEqual(result.contents, { 'a': 3, 'b': 2, 'c': 4 });
assert.deepEqual(result.keys, ['a', 'b', 'c']);
});
test('recursive merge', () => {
let base = new ConfigurationModel({ 'a': { 'b': 1 } }, ['a.b']);
let add = new ConfigurationModel({ 'a': { 'b': 2 } }, ['a.b']);
let result = base.merge(add);
assert.deepEqual(result.contents, { 'a': { 'b': 2 } });
assert.deepEqual(result.getValue('a'), { 'b': 2 });
assert.deepEqual(result.keys, ['a.b']);
});
test('simple merge overrides', () => {
let base = new ConfigurationModel({ 'a': { 'b': 1 } }, ['a.b'], [{ identifiers: ['c'], contents: { 'a': 2 }, keys: ['a'] }]);
let add = new ConfigurationModel({ 'a': { 'b': 2 } }, ['a.b'], [{ identifiers: ['c'], contents: { 'b': 2 }, keys: ['b'] }]);
let result = base.merge(add);
assert.deepEqual(result.contents, { 'a': { 'b': 2 } });
assert.deepEqual(result.overrides, [{ identifiers: ['c'], contents: { 'a': 2, 'b': 2 }, keys: ['a'] }]);
assert.deepEqual(result.override('c').contents, { 'a': 2, 'b': 2 });
assert.deepEqual(result.keys, ['a.b']);
});
test('recursive merge overrides', () => {
let base = new ConfigurationModel({ 'a': { 'b': 1 }, 'f': 1 }, ['a.b', 'f'], [{ identifiers: ['c'], contents: { 'a': { 'd': 1 } }, keys: ['a'] }]);
let add = new ConfigurationModel({ 'a': { 'b': 2 } }, ['a.b'], [{ identifiers: ['c'], contents: { 'a': { 'e': 2 } }, keys: ['a'] }]);
let result = base.merge(add);
assert.deepEqual(result.contents, { 'a': { 'b': 2 }, 'f': 1 });
assert.deepEqual(result.overrides, [{ identifiers: ['c'], contents: { 'a': { 'd': 1, 'e': 2 } }, keys: ['a'] }]);
assert.deepEqual(result.override('c').contents, { 'a': { 'b': 2, 'd': 1, 'e': 2 }, 'f': 1 });
assert.deepEqual(result.keys, ['a.b', 'f']);
});
test('merge overrides when frozen', () => {
let model1 = new ConfigurationModel({ 'a': { 'b': 1 }, 'f': 1 }, ['a.b', 'f'], [{ identifiers: ['c'], contents: { 'a': { 'd': 1 } }, keys: ['a'] }]).freeze();
let model2 = new ConfigurationModel({ 'a': { 'b': 2 } }, ['a.b'], [{ identifiers: ['c'], contents: { 'a': { 'e': 2 } }, keys: ['a'] }]).freeze();
let result = new ConfigurationModel().merge(model1, model2);
assert.deepEqual(result.contents, { 'a': { 'b': 2 }, 'f': 1 });
assert.deepEqual(result.overrides, [{ identifiers: ['c'], contents: { 'a': { 'd': 1, 'e': 2 } }, keys: ['a'] }]);
assert.deepEqual(result.override('c').contents, { 'a': { 'b': 2, 'd': 1, 'e': 2 }, 'f': 1 });
assert.deepEqual(result.keys, ['a.b', 'f']);
});
test('Test contents while getting an existing property', () => {
let testObject = new ConfigurationModel({ 'a': 1 });
assert.deepEqual(testObject.getValue('a'), 1);
testObject = new ConfigurationModel({ 'a': { 'b': 1 } });
assert.deepEqual(testObject.getValue('a'), { 'b': 1 });
});
test('Test contents are undefined for non existing properties', () => {
const testObject = new ConfigurationModel({ awesome: true });
assert.deepEqual(testObject.getValue('unknownproperty'), undefined);
});
test('Test override gives all content merged with overrides', () => {
const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, [], [{ identifiers: ['b'], contents: { 'a': 2 }, keys: ['a'] }]);
assert.deepEqual(testObject.override('b').contents, { 'a': 2, 'c': 1 });
});
});
suite('CustomConfigurationModel', () => {
test('simple merge using models', () => {
let base = new ConfigurationModelParser('base');
base.parseContent(JSON.stringify({ 'a': 1, 'b': 2 }));
let add = new ConfigurationModelParser('add');
add.parseContent(JSON.stringify({ 'a': 3, 'c': 4 }));
let result = base.configurationModel.merge(add.configurationModel);
assert.deepEqual(result.contents, { 'a': 3, 'b': 2, 'c': 4 });
});
test('simple merge with an undefined contents', () => {
let base = new ConfigurationModelParser('base');
base.parseContent(JSON.stringify({ 'a': 1, 'b': 2 }));
let add = new ConfigurationModelParser('add');
let result = base.configurationModel.merge(add.configurationModel);
assert.deepEqual(result.contents, { 'a': 1, 'b': 2 });
base = new ConfigurationModelParser('base');
add = new ConfigurationModelParser('add');
add.parseContent(JSON.stringify({ 'a': 1, 'b': 2 }));
result = base.configurationModel.merge(add.configurationModel);
assert.deepEqual(result.contents, { 'a': 1, 'b': 2 });
base = new ConfigurationModelParser('base');
add = new ConfigurationModelParser('add');
result = base.configurationModel.merge(add.configurationModel);
assert.deepEqual(result.contents, {});
});
test('Recursive merge using config models', () => {
let base = new ConfigurationModelParser('base');
base.parseContent(JSON.stringify({ 'a': { 'b': 1 } }));
let add = new ConfigurationModelParser('add');
add.parseContent(JSON.stringify({ 'a': { 'b': 2 } }));
let result = base.configurationModel.merge(add.configurationModel);
assert.deepEqual(result.contents, { 'a': { 'b': 2 } });
});
test('Test contents while getting an existing property', () => {
let testObject = new ConfigurationModelParser('test');
testObject.parseContent(JSON.stringify({ 'a': 1 }));
assert.deepEqual(testObject.configurationModel.getValue('a'), 1);
testObject.parseContent(JSON.stringify({ 'a': { 'b': 1 } }));
assert.deepEqual(testObject.configurationModel.getValue('a'), { 'b': 1 });
});
test('Test contents are undefined for non existing properties', () => {
const testObject = new ConfigurationModelParser('test');
testObject.parseContent(JSON.stringify({
awesome: true
}));
assert.deepEqual(testObject.configurationModel.getValue('unknownproperty'), undefined);
});
test('Test contents are undefined for undefined config', () => {
const testObject = new ConfigurationModelParser('test');
assert.deepEqual(testObject.configurationModel.getValue('unknownproperty'), undefined);
});
test('Test configWithOverrides gives all content merged with overrides', () => {
const testObject = new ConfigurationModelParser('test');
testObject.parseContent(JSON.stringify({ 'a': 1, 'c': 1, '[b]': { 'a': 2 } }));
assert.deepEqual(testObject.configurationModel.override('b').contents, { 'a': 2, 'c': 1, '[b]': { 'a': 2 } });
});
test('Test configWithOverrides gives empty contents', () => {
const testObject = new ConfigurationModelParser('test');
assert.deepEqual(testObject.configurationModel.override('b').contents, {});
});
test('Test update with empty data', () => {
const testObject = new ConfigurationModelParser('test');
testObject.parseContent('');
assert.deepEqual(testObject.configurationModel.contents, {});
assert.deepEqual(testObject.configurationModel.keys, []);
testObject.parseContent(null!);
assert.deepEqual(testObject.configurationModel.contents, {});
assert.deepEqual(testObject.configurationModel.keys, []);
testObject.parseContent(undefined!);
assert.deepEqual(testObject.configurationModel.contents, {});
assert.deepEqual(testObject.configurationModel.keys, []);
});
test('Test registering the same property again', () => {
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
'id': 'a',
'order': 1,
'title': 'a',
'type': 'object',
'properties': {
'a': {
'description': 'a',
'type': 'boolean',
'default': true,
}
}
});
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
'id': 'a',
'order': 1,
'title': 'a',
'type': 'object',
'properties': {
'a': {
'description': 'a',
'type': 'boolean',
'default': false,
}
}
});
assert.equal(true, new DefaultConfigurationModel().getValue('a'));
});
});
suite('Configuration', () => {
test('Test inspect for overrideIdentifiers', () => {
const defaultConfigurationModel = parseConfigurationModel({ '[l1]': { 'a': 1 }, '[l2]': { 'b': 1 } });
const userConfigurationModel = parseConfigurationModel({ '[l3]': { 'a': 2 } });
const workspaceConfigurationModel = parseConfigurationModel({ '[l1]': { 'a': 3 }, '[l4]': { 'a': 3 } });
const testObject: Configuration = new Configuration(defaultConfigurationModel, userConfigurationModel, new ConfigurationModel(), workspaceConfigurationModel);
const { overrideIdentifiers } = testObject.inspect('a', {}, undefined);
assert.deepEqual(overrideIdentifiers, ['l1', 'l3', 'l4']);
});
test('Test update value', () => {
const parser = new ConfigurationModelParser('test');
parser.parseContent(JSON.stringify({ 'a': 1 }));
const testObject: Configuration = new Configuration(parser.configurationModel, new ConfigurationModel());
testObject.updateValue('a', 2);
assert.equal(testObject.getValue('a', {}, undefined), 2);
});
test('Test update value after inspect', () => {
const parser = new ConfigurationModelParser('test');
parser.parseContent(JSON.stringify({ 'a': 1 }));
const testObject: Configuration = new Configuration(parser.configurationModel, new ConfigurationModel());
testObject.inspect('a', {}, undefined);
testObject.updateValue('a', 2);
assert.equal(testObject.getValue('a', {}, undefined), 2);
});
test('Test compare and update default configuration', () => {
const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel());
testObject.updateDefaultConfiguration(toConfigurationModel({
'editor.lineNumbers': 'on',
}));
const actual = testObject.compareAndUpdateDefaultConfiguration(toConfigurationModel({
'editor.lineNumbers': 'off',
'[markdown]': {
'editor.wordWrap': 'off'
}
}), ['editor.lineNumbers', '[markdown]']);
assert.deepEqual(actual, { keys: ['editor.lineNumbers', '[markdown]'], overrides: [['markdown', ['editor.wordWrap']]] });
});
test('Test compare and update user configuration', () => {
const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel());
testObject.updateLocalUserConfiguration(toConfigurationModel({
'editor.lineNumbers': 'off',
'editor.fontSize': 12,
'[typescript]': {
'editor.wordWrap': 'off'
}
}));
const actual = testObject.compareAndUpdateLocalUserConfiguration(toConfigurationModel({
'editor.lineNumbers': 'on',
'window.zoomLevel': 1,
'[typescript]': {
'editor.wordWrap': 'on',
'editor.insertSpaces': false
}
}));
assert.deepEqual(actual, { keys: ['window.zoomLevel', 'editor.lineNumbers', '[typescript]', 'editor.fontSize'], overrides: [['typescript', ['editor.insertSpaces', 'editor.wordWrap']]] });
});
test('Test compare and update workspace configuration', () => {
const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel());
testObject.updateWorkspaceConfiguration(toConfigurationModel({
'editor.lineNumbers': 'off',
'editor.fontSize': 12,
'[typescript]': {
'editor.wordWrap': 'off'
}
}));
const actual = testObject.compareAndUpdateWorkspaceConfiguration(toConfigurationModel({
'editor.lineNumbers': 'on',
'window.zoomLevel': 1,
'[typescript]': {
'editor.wordWrap': 'on',
'editor.insertSpaces': false
}
}));
assert.deepEqual(actual, { keys: ['window.zoomLevel', 'editor.lineNumbers', '[typescript]', 'editor.fontSize'], overrides: [['typescript', ['editor.insertSpaces', 'editor.wordWrap']]] });
});
test('Test compare and update workspace folder configuration', () => {
const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel());
testObject.updateFolderConfiguration(URI.file('file1'), toConfigurationModel({
'editor.lineNumbers': 'off',
'editor.fontSize': 12,
'[typescript]': {
'editor.wordWrap': 'off'
}
}));
const actual = testObject.compareAndUpdateFolderConfiguration(URI.file('file1'), toConfigurationModel({
'editor.lineNumbers': 'on',
'window.zoomLevel': 1,
'[typescript]': {
'editor.wordWrap': 'on',
'editor.insertSpaces': false
}
}));
assert.deepEqual(actual, { keys: ['window.zoomLevel', 'editor.lineNumbers', '[typescript]', 'editor.fontSize'], overrides: [['typescript', ['editor.insertSpaces', 'editor.wordWrap']]] });
});
test('Test compare and delete workspace folder configuration', () => {
const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel());
testObject.updateFolderConfiguration(URI.file('file1'), toConfigurationModel({
'editor.lineNumbers': 'off',
'editor.fontSize': 12,
'[typescript]': {
'editor.wordWrap': 'off'
}
}));
const actual = testObject.compareAndDeleteFolderConfiguration(URI.file('file1'));
assert.deepEqual(actual, { keys: ['editor.lineNumbers', 'editor.fontSize', '[typescript]'], overrides: [['typescript', ['editor.wordWrap']]] });
});
function parseConfigurationModel(content: any): ConfigurationModel {
const parser = new ConfigurationModelParser('test');
parser.parseContent(JSON.stringify(content));
return parser.configurationModel;
}
});
suite('ConfigurationChangeEvent', () => {
test('changeEvent affecting keys with new configuration', () => {
const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel());
const change = configuration.compareAndUpdateLocalUserConfiguration(toConfigurationModel({
'window.zoomLevel': 1,
'workbench.editor.enablePreview': false,
'files.autoSave': 'off',
}));
let testObject = new ConfigurationChangeEvent(change, undefined, configuration);
assert.deepEqual(testObject.affectedKeys, ['window.zoomLevel', 'workbench.editor.enablePreview', 'files.autoSave']);
assert.ok(testObject.affectsConfiguration('window.zoomLevel'));
assert.ok(testObject.affectsConfiguration('window'));
assert.ok(testObject.affectsConfiguration('workbench.editor.enablePreview'));
assert.ok(testObject.affectsConfiguration('workbench.editor'));
assert.ok(testObject.affectsConfiguration('workbench'));
assert.ok(testObject.affectsConfiguration('files'));
assert.ok(testObject.affectsConfiguration('files.autoSave'));
assert.ok(!testObject.affectsConfiguration('files.exclude'));
assert.ok(!testObject.affectsConfiguration('[markdown]'));
assert.ok(!testObject.affectsConfiguration('editor'));
});
test('changeEvent affecting keys when configuration changed', () => {
const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel());
configuration.updateLocalUserConfiguration(toConfigurationModel({
'window.zoomLevel': 2,
'workbench.editor.enablePreview': true,
'files.autoSave': 'off',
}));
const data = configuration.toData();
const change = configuration.compareAndUpdateLocalUserConfiguration(toConfigurationModel({
'window.zoomLevel': 1,
'workbench.editor.enablePreview': false,
'files.autoSave': 'off',
}));
let testObject = new ConfigurationChangeEvent(change, { data }, configuration);
assert.deepEqual(testObject.affectedKeys, ['window.zoomLevel', 'workbench.editor.enablePreview']);
assert.ok(testObject.affectsConfiguration('window.zoomLevel'));
assert.ok(testObject.affectsConfiguration('window'));
assert.ok(testObject.affectsConfiguration('workbench.editor.enablePreview'));
assert.ok(testObject.affectsConfiguration('workbench.editor'));
assert.ok(testObject.affectsConfiguration('workbench'));
assert.ok(!testObject.affectsConfiguration('files'));
assert.ok(!testObject.affectsConfiguration('[markdown]'));
assert.ok(!testObject.affectsConfiguration('editor'));
});
test('changeEvent affecting overrides with new configuration', () => {
const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel());
const change = configuration.compareAndUpdateLocalUserConfiguration(toConfigurationModel({
'files.autoSave': 'off',
'[markdown]': {
'editor.wordWrap': 'off'
}
}));
let testObject = new ConfigurationChangeEvent(change, undefined, configuration);
assert.deepEqual(testObject.affectedKeys, ['files.autoSave', '[markdown]', 'editor.wordWrap']);
assert.ok(testObject.affectsConfiguration('files'));
assert.ok(testObject.affectsConfiguration('files.autoSave'));
assert.ok(!testObject.affectsConfiguration('files.exclude'));
assert.ok(testObject.affectsConfiguration('[markdown]'));
assert.ok(!testObject.affectsConfiguration('[markdown].editor'));
assert.ok(!testObject.affectsConfiguration('[markdown].workbench'));
assert.ok(testObject.affectsConfiguration('editor'));
assert.ok(testObject.affectsConfiguration('editor.wordWrap'));
assert.ok(testObject.affectsConfiguration('editor', { overrideIdentifier: 'markdown' }));
assert.ok(testObject.affectsConfiguration('editor.wordWrap', { overrideIdentifier: 'markdown' }));
assert.ok(!testObject.affectsConfiguration('editor', { overrideIdentifier: 'json' }));
assert.ok(!testObject.affectsConfiguration('editor.fontSize', { overrideIdentifier: 'markdown' }));
assert.ok(!testObject.affectsConfiguration('editor.fontSize'));
assert.ok(!testObject.affectsConfiguration('window'));
});
test('changeEvent affecting overrides when configuration changed', () => {
const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel());
configuration.updateLocalUserConfiguration(toConfigurationModel({
'workbench.editor.enablePreview': true,
'[markdown]': {
'editor.fontSize': 12,
'editor.wordWrap': 'off'
},
'files.autoSave': 'off',
}));
const data = configuration.toData();
const change = configuration.compareAndUpdateLocalUserConfiguration(toConfigurationModel({
'files.autoSave': 'off',
'[markdown]': {
'editor.fontSize': 13,
'editor.wordWrap': 'off'
},
'window.zoomLevel': 1,
}));
let testObject = new ConfigurationChangeEvent(change, { data }, configuration);
assert.deepEqual(testObject.affectedKeys, ['window.zoomLevel', '[markdown]', 'workbench.editor.enablePreview', 'editor.fontSize']);
assert.ok(!testObject.affectsConfiguration('files'));
assert.ok(testObject.affectsConfiguration('[markdown]'));
assert.ok(!testObject.affectsConfiguration('[markdown].editor'));
assert.ok(!testObject.affectsConfiguration('[markdown].editor.fontSize'));
assert.ok(!testObject.affectsConfiguration('[markdown].editor.wordWrap'));
assert.ok(!testObject.affectsConfiguration('[markdown].workbench'));
assert.ok(testObject.affectsConfiguration('editor'));
assert.ok(testObject.affectsConfiguration('editor', { overrideIdentifier: 'markdown' }));
assert.ok(testObject.affectsConfiguration('editor.fontSize', { overrideIdentifier: 'markdown' }));
assert.ok(!testObject.affectsConfiguration('editor.wordWrap'));
assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { overrideIdentifier: 'markdown' }));
assert.ok(!testObject.affectsConfiguration('editor', { overrideIdentifier: 'json' }));
assert.ok(!testObject.affectsConfiguration('editor.fontSize', { overrideIdentifier: 'json' }));
assert.ok(testObject.affectsConfiguration('window'));
assert.ok(testObject.affectsConfiguration('window.zoomLevel'));
assert.ok(testObject.affectsConfiguration('window', { overrideIdentifier: 'markdown' }));
assert.ok(testObject.affectsConfiguration('window.zoomLevel', { overrideIdentifier: 'markdown' }));
assert.ok(testObject.affectsConfiguration('workbench'));
assert.ok(testObject.affectsConfiguration('workbench.editor'));
assert.ok(testObject.affectsConfiguration('workbench.editor.enablePreview'));
assert.ok(testObject.affectsConfiguration('workbench', { overrideIdentifier: 'markdown' }));
assert.ok(testObject.affectsConfiguration('workbench.editor', { overrideIdentifier: 'markdown' }));
});
test('changeEvent affecting workspace folders', () => {
const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel());
configuration.updateWorkspaceConfiguration(toConfigurationModel({ 'window.title': 'custom' }));
configuration.updateFolderConfiguration(URI.file('folder1'), toConfigurationModel({ 'window.zoomLevel': 2, 'window.restoreFullscreen': true }));
configuration.updateFolderConfiguration(URI.file('folder2'), toConfigurationModel({ 'workbench.editor.enablePreview': true, 'window.restoreWindows': true }));
const data = configuration.toData();
const workspace = new Workspace('a', [new WorkspaceFolder({ index: 0, name: 'a', uri: URI.file('folder1') }), new WorkspaceFolder({ index: 1, name: 'b', uri: URI.file('folder2') }), new WorkspaceFolder({ index: 2, name: 'c', uri: URI.file('folder3') })]);
const change = mergeChanges(
configuration.compareAndUpdateWorkspaceConfiguration(toConfigurationModel({ 'window.title': 'native' })),
configuration.compareAndUpdateFolderConfiguration(URI.file('folder1'), toConfigurationModel({ 'window.zoomLevel': 1, 'window.restoreFullscreen': false })),
configuration.compareAndUpdateFolderConfiguration(URI.file('folder2'), toConfigurationModel({ 'workbench.editor.enablePreview': false, 'window.restoreWindows': false }))
);
let testObject = new ConfigurationChangeEvent(change, { data, workspace }, configuration, workspace);
assert.deepEqual(testObject.affectedKeys, ['window.title', 'window.zoomLevel', 'window.restoreFullscreen', 'workbench.editor.enablePreview', 'window.restoreWindows']);
assert.ok(testObject.affectsConfiguration('window.zoomLevel'));
assert.ok(testObject.affectsConfiguration('window.zoomLevel', { resource: URI.file('folder1') }));
assert.ok(testObject.affectsConfiguration('window.zoomLevel', { resource: URI.file(join('folder1', 'file1')) }));
assert.ok(!testObject.affectsConfiguration('window.zoomLevel', { resource: URI.file('file1') }));
assert.ok(!testObject.affectsConfiguration('window.zoomLevel', { resource: URI.file('file2') }));
assert.ok(!testObject.affectsConfiguration('window.zoomLevel', { resource: URI.file(join('folder2', 'file2')) }));
assert.ok(!testObject.affectsConfiguration('window.zoomLevel', { resource: URI.file(join('folder3', 'file3')) }));
assert.ok(testObject.affectsConfiguration('window.restoreFullscreen'));
assert.ok(testObject.affectsConfiguration('window.restoreFullscreen', { resource: URI.file(join('folder1', 'file1')) }));
assert.ok(testObject.affectsConfiguration('window.restoreFullscreen', { resource: URI.file('folder1') }));
assert.ok(!testObject.affectsConfiguration('window.restoreFullscreen', { resource: URI.file('file1') }));
assert.ok(!testObject.affectsConfiguration('window.restoreFullscreen', { resource: URI.file('file2') }));
assert.ok(!testObject.affectsConfiguration('window.restoreFullscreen', { resource: URI.file(join('folder2', 'file2')) }));
assert.ok(!testObject.affectsConfiguration('window.restoreFullscreen', { resource: URI.file(join('folder3', 'file3')) }));
assert.ok(testObject.affectsConfiguration('window.restoreWindows'));
assert.ok(testObject.affectsConfiguration('window.restoreWindows', { resource: URI.file('folder2') }));
assert.ok(testObject.affectsConfiguration('window.restoreWindows', { resource: URI.file(join('folder2', 'file2')) }));
assert.ok(!testObject.affectsConfiguration('window.restoreWindows', { resource: URI.file('file2') }));
assert.ok(!testObject.affectsConfiguration('window.restoreWindows', { resource: URI.file(join('folder1', 'file1')) }));
assert.ok(!testObject.affectsConfiguration('window.restoreWindows', { resource: URI.file(join('folder3', 'file3')) }));
assert.ok(testObject.affectsConfiguration('window.title'));
assert.ok(testObject.affectsConfiguration('window.title', { resource: URI.file('folder1') }));
assert.ok(testObject.affectsConfiguration('window.title', { resource: URI.file(join('folder1', 'file1')) }));
assert.ok(testObject.affectsConfiguration('window.title', { resource: URI.file('folder2') }));
assert.ok(testObject.affectsConfiguration('window.title', { resource: URI.file(join('folder2', 'file2')) }));
assert.ok(testObject.affectsConfiguration('window.title', { resource: URI.file('folder3') }));
assert.ok(testObject.affectsConfiguration('window.title', { resource: URI.file(join('folder3', 'file3')) }));
assert.ok(testObject.affectsConfiguration('window.title', { resource: URI.file('file1') }));
assert.ok(testObject.affectsConfiguration('window.title', { resource: URI.file('file2') }));
assert.ok(testObject.affectsConfiguration('window.title', { resource: URI.file('file3') }));
assert.ok(testObject.affectsConfiguration('window'));
assert.ok(testObject.affectsConfiguration('window', { resource: URI.file('folder1') }));
assert.ok(testObject.affectsConfiguration('window', { resource: URI.file(join('folder1', 'file1')) }));
assert.ok(testObject.affectsConfiguration('window', { resource: URI.file('folder2') }));
assert.ok(testObject.affectsConfiguration('window', { resource: URI.file(join('folder2', 'file2')) }));
assert.ok(testObject.affectsConfiguration('window', { resource: URI.file('folder3') }));
assert.ok(testObject.affectsConfiguration('window', { resource: URI.file(join('folder3', 'file3')) }));
assert.ok(testObject.affectsConfiguration('window', { resource: URI.file('file1') }));
assert.ok(testObject.affectsConfiguration('window', { resource: URI.file('file2') }));
assert.ok(testObject.affectsConfiguration('window', { resource: URI.file('file3') }));
assert.ok(testObject.affectsConfiguration('workbench.editor.enablePreview'));
assert.ok(testObject.affectsConfiguration('workbench.editor.enablePreview', { resource: URI.file('folder2') }));
assert.ok(testObject.affectsConfiguration('workbench.editor.enablePreview', { resource: URI.file(join('folder2', 'file2')) }));
assert.ok(!testObject.affectsConfiguration('workbench.editor.enablePreview', { resource: URI.file('folder1') }));
assert.ok(!testObject.affectsConfiguration('workbench.editor.enablePreview', { resource: URI.file(join('folder1', 'file1')) }));
assert.ok(!testObject.affectsConfiguration('workbench.editor.enablePreview', { resource: URI.file('folder3') }));
assert.ok(testObject.affectsConfiguration('workbench.editor'));
assert.ok(testObject.affectsConfiguration('workbench.editor', { resource: URI.file('folder2') }));
assert.ok(testObject.affectsConfiguration('workbench.editor', { resource: URI.file(join('folder2', 'file2')) }));
assert.ok(!testObject.affectsConfiguration('workbench.editor', { resource: URI.file('folder1') }));
assert.ok(!testObject.affectsConfiguration('workbench.editor', { resource: URI.file(join('folder1', 'file1')) }));
assert.ok(!testObject.affectsConfiguration('workbench.editor', { resource: URI.file('folder3') }));
assert.ok(testObject.affectsConfiguration('workbench'));
assert.ok(testObject.affectsConfiguration('workbench', { resource: URI.file('folder2') }));
assert.ok(testObject.affectsConfiguration('workbench', { resource: URI.file(join('folder2', 'file2')) }));
assert.ok(!testObject.affectsConfiguration('workbench', { resource: URI.file('folder1') }));
assert.ok(!testObject.affectsConfiguration('workbench', { resource: URI.file('folder3') }));
assert.ok(!testObject.affectsConfiguration('files'));
assert.ok(!testObject.affectsConfiguration('files', { resource: URI.file('folder1') }));
assert.ok(!testObject.affectsConfiguration('files', { resource: URI.file(join('folder1', 'file1')) }));
assert.ok(!testObject.affectsConfiguration('files', { resource: URI.file('folder2') }));
assert.ok(!testObject.affectsConfiguration('files', { resource: URI.file(join('folder2', 'file2')) }));
assert.ok(!testObject.affectsConfiguration('files', { resource: URI.file('folder3') }));
assert.ok(!testObject.affectsConfiguration('files', { resource: URI.file(join('folder3', 'file3')) }));
});
test('changeEvent - all', () => {
const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel());
configuration.updateFolderConfiguration(URI.file('file1'), toConfigurationModel({ 'window.zoomLevel': 2, 'window.restoreFullscreen': true }));
const data = configuration.toData();
const change = mergeChanges(
configuration.compareAndUpdateDefaultConfiguration(toConfigurationModel({
'editor.lineNumbers': 'off',
'[markdown]': {
'editor.wordWrap': 'off'
}
}), ['editor.lineNumbers', '[markdown]']),
configuration.compareAndUpdateLocalUserConfiguration(toConfigurationModel({
'[json]': {
'editor.lineNumbers': 'relative'
}
})),
configuration.compareAndUpdateWorkspaceConfiguration(toConfigurationModel({ 'window.title': 'custom' })),
configuration.compareAndDeleteFolderConfiguration(URI.file('file1')),
configuration.compareAndUpdateFolderConfiguration(URI.file('file2'), toConfigurationModel({ 'workbench.editor.enablePreview': true, 'window.restoreWindows': true })));
const workspace = new Workspace('a', [new WorkspaceFolder({ index: 0, name: 'a', uri: URI.file('file1') }), new WorkspaceFolder({ index: 1, name: 'b', uri: URI.file('file2') }), new WorkspaceFolder({ index: 2, name: 'c', uri: URI.file('folder3') })]);
const testObject = new ConfigurationChangeEvent(change, { data, workspace }, configuration, workspace);
assert.deepEqual(testObject.affectedKeys, ['editor.lineNumbers', '[markdown]', '[json]', 'window.title', 'window.zoomLevel', 'window.restoreFullscreen', 'workbench.editor.enablePreview', 'window.restoreWindows', 'editor.wordWrap']);
assert.ok(testObject.affectsConfiguration('window.title'));
assert.ok(testObject.affectsConfiguration('window.title', { resource: URI.file('file1') }));
assert.ok(testObject.affectsConfiguration('window.title', { resource: URI.file('file2') }));
assert.ok(testObject.affectsConfiguration('window'));
assert.ok(testObject.affectsConfiguration('window', { resource: URI.file('file1') }));
assert.ok(testObject.affectsConfiguration('window', { resource: URI.file('file2') }));
assert.ok(testObject.affectsConfiguration('window.zoomLevel'));
assert.ok(testObject.affectsConfiguration('window.zoomLevel', { resource: URI.file('file1') }));
assert.ok(!testObject.affectsConfiguration('window.zoomLevel', { resource: URI.file('file2') }));
assert.ok(testObject.affectsConfiguration('window.restoreFullscreen'));
assert.ok(testObject.affectsConfiguration('window.restoreFullscreen', { resource: URI.file('file1') }));
assert.ok(!testObject.affectsConfiguration('window.restoreFullscreen', { resource: URI.file('file2') }));
assert.ok(testObject.affectsConfiguration('window.restoreWindows'));
assert.ok(testObject.affectsConfiguration('window.restoreWindows', { resource: URI.file('file2') }));
assert.ok(!testObject.affectsConfiguration('window.restoreWindows', { resource: URI.file('file1') }));
assert.ok(testObject.affectsConfiguration('workbench.editor.enablePreview'));
assert.ok(testObject.affectsConfiguration('workbench.editor.enablePreview', { resource: URI.file('file2') }));
assert.ok(!testObject.affectsConfiguration('workbench.editor.enablePreview', { resource: URI.file('file1') }));
assert.ok(testObject.affectsConfiguration('workbench.editor'));
assert.ok(testObject.affectsConfiguration('workbench.editor', { resource: URI.file('file2') }));
assert.ok(!testObject.affectsConfiguration('workbench.editor', { resource: URI.file('file1') }));
assert.ok(testObject.affectsConfiguration('workbench'));
assert.ok(testObject.affectsConfiguration('workbench', { resource: URI.file('file2') }));
assert.ok(!testObject.affectsConfiguration('workbench', { resource: URI.file('file1') }));
assert.ok(!testObject.affectsConfiguration('files'));
assert.ok(!testObject.affectsConfiguration('files', { resource: URI.file('file1') }));
assert.ok(!testObject.affectsConfiguration('files', { resource: URI.file('file2') }));
assert.ok(testObject.affectsConfiguration('editor'));
assert.ok(testObject.affectsConfiguration('editor', { resource: URI.file('file1') }));
assert.ok(testObject.affectsConfiguration('editor', { resource: URI.file('file2') }));
assert.ok(testObject.affectsConfiguration('editor', { resource: URI.file('file1'), overrideIdentifier: 'json' }));
assert.ok(testObject.affectsConfiguration('editor', { resource: URI.file('file1'), overrideIdentifier: 'markdown' }));
assert.ok(testObject.affectsConfiguration('editor', { resource: URI.file('file1'), overrideIdentifier: 'typescript' }));
assert.ok(testObject.affectsConfiguration('editor', { resource: URI.file('file2'), overrideIdentifier: 'json' }));
assert.ok(testObject.affectsConfiguration('editor', { resource: URI.file('file2'), overrideIdentifier: 'markdown' }));
assert.ok(testObject.affectsConfiguration('editor', { resource: URI.file('file2'), overrideIdentifier: 'typescript' }));
assert.ok(testObject.affectsConfiguration('editor.lineNumbers'));
assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { resource: URI.file('file1') }));
assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { resource: URI.file('file2') }));
assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { resource: URI.file('file1'), overrideIdentifier: 'json' }));
assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { resource: URI.file('file1'), overrideIdentifier: 'markdown' }));
assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { resource: URI.file('file1'), overrideIdentifier: 'typescript' }));
assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { resource: URI.file('file2'), overrideIdentifier: 'json' }));
assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { resource: URI.file('file2'), overrideIdentifier: 'markdown' }));
assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { resource: URI.file('file2'), overrideIdentifier: 'typescript' }));
assert.ok(testObject.affectsConfiguration('editor.wordWrap'));
assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { resource: URI.file('file1') }));
assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { resource: URI.file('file2') }));
assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { resource: URI.file('file1'), overrideIdentifier: 'json' }));
assert.ok(testObject.affectsConfiguration('editor.wordWrap', { resource: URI.file('file1'), overrideIdentifier: 'markdown' }));
assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { resource: URI.file('file1'), overrideIdentifier: 'typescript' }));
assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { resource: URI.file('file2'), overrideIdentifier: 'json' }));
assert.ok(testObject.affectsConfiguration('editor.wordWrap', { resource: URI.file('file2'), overrideIdentifier: 'markdown' }));
assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { resource: URI.file('file2'), overrideIdentifier: 'typescript' }));
assert.ok(!testObject.affectsConfiguration('editor.fontSize'));
assert.ok(!testObject.affectsConfiguration('editor.fontSize', { resource: URI.file('file1') }));
assert.ok(!testObject.affectsConfiguration('editor.fontSize', { resource: URI.file('file2') }));
});
test('changeEvent affecting tasks and launches', () => {
const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel());
const change = configuration.compareAndUpdateLocalUserConfiguration(toConfigurationModel({
'launch': {
'configuraiton': {}
},
'launch.version': 1,
'tasks': {
'version': 2
}
}));
let testObject = new ConfigurationChangeEvent(change, undefined, configuration);
assert.deepEqual(testObject.affectedKeys, ['launch', 'launch.version', 'tasks']);
assert.ok(testObject.affectsConfiguration('launch'));
assert.ok(testObject.affectsConfiguration('launch.version'));
assert.ok(testObject.affectsConfiguration('tasks'));
});
});
suite('AllKeysConfigurationChangeEvent', () => {
test('changeEvent', () => {
const configuration = new Configuration(new ConfigurationModel(), new ConfigurationModel());
configuration.updateDefaultConfiguration(toConfigurationModel({
'editor.lineNumbers': 'off',
'[markdown]': {
'editor.wordWrap': 'off'
}
}));
configuration.updateLocalUserConfiguration(toConfigurationModel({
'[json]': {
'editor.lineNumbers': 'relative'
}
}));
configuration.updateWorkspaceConfiguration(toConfigurationModel({ 'window.title': 'custom' }));
configuration.updateFolderConfiguration(URI.file('file1'), toConfigurationModel({ 'window.zoomLevel': 2, 'window.restoreFullscreen': true }));
configuration.updateFolderConfiguration(URI.file('file2'), toConfigurationModel({ 'workbench.editor.enablePreview': true, 'window.restoreWindows': true }));
const workspace = new Workspace('a', [new WorkspaceFolder({ index: 0, name: 'a', uri: URI.file('file1') }), new WorkspaceFolder({ index: 1, name: 'b', uri: URI.file('file2') }), new WorkspaceFolder({ index: 2, name: 'c', uri: URI.file('folder3') })]);
let testObject = new AllKeysConfigurationChangeEvent(configuration, workspace, ConfigurationTarget.USER, null);
assert.deepEqual(testObject.affectedKeys, ['editor.lineNumbers', '[markdown]', '[json]', 'window.title', 'window.zoomLevel', 'window.restoreFullscreen', 'workbench.editor.enablePreview', 'window.restoreWindows']);
assert.ok(testObject.affectsConfiguration('window.title'));
assert.ok(testObject.affectsConfiguration('window.title', { resource: URI.file('file1') }));
assert.ok(testObject.affectsConfiguration('window.title', { resource: URI.file('file2') }));
assert.ok(testObject.affectsConfiguration('window'));
assert.ok(testObject.affectsConfiguration('window', { resource: URI.file('file1') }));
assert.ok(testObject.affectsConfiguration('window', { resource: URI.file('file2') }));
assert.ok(testObject.affectsConfiguration('window.zoomLevel'));
assert.ok(testObject.affectsConfiguration('window.zoomLevel', { resource: URI.file('file1') }));
assert.ok(!testObject.affectsConfiguration('window.zoomLevel', { resource: URI.file('file2') }));
assert.ok(testObject.affectsConfiguration('window.restoreFullscreen'));
assert.ok(testObject.affectsConfiguration('window.restoreFullscreen', { resource: URI.file('file1') }));
assert.ok(!testObject.affectsConfiguration('window.restoreFullscreen', { resource: URI.file('file2') }));
assert.ok(testObject.affectsConfiguration('window.restoreWindows'));
assert.ok(testObject.affectsConfiguration('window.restoreWindows', { resource: URI.file('file2') }));
assert.ok(!testObject.affectsConfiguration('window.restoreWindows', { resource: URI.file('file1') }));
assert.ok(testObject.affectsConfiguration('workbench.editor.enablePreview'));
assert.ok(testObject.affectsConfiguration('workbench.editor.enablePreview', { resource: URI.file('file2') }));
assert.ok(!testObject.affectsConfiguration('workbench.editor.enablePreview', { resource: URI.file('file1') }));
assert.ok(testObject.affectsConfiguration('workbench.editor'));
assert.ok(testObject.affectsConfiguration('workbench.editor', { resource: URI.file('file2') }));
assert.ok(!testObject.affectsConfiguration('workbench.editor', { resource: URI.file('file1') }));
assert.ok(testObject.affectsConfiguration('workbench'));
assert.ok(testObject.affectsConfiguration('workbench', { resource: URI.file('file2') }));
assert.ok(!testObject.affectsConfiguration('workbench', { resource: URI.file('file1') }));
assert.ok(!testObject.affectsConfiguration('files'));
assert.ok(!testObject.affectsConfiguration('files', { resource: URI.file('file1') }));
assert.ok(!testObject.affectsConfiguration('files', { resource: URI.file('file2') }));
assert.ok(testObject.affectsConfiguration('editor'));
assert.ok(testObject.affectsConfiguration('editor', { resource: URI.file('file1') }));
assert.ok(testObject.affectsConfiguration('editor', { resource: URI.file('file2') }));
assert.ok(testObject.affectsConfiguration('editor', { resource: URI.file('file1'), overrideIdentifier: 'json' }));
assert.ok(testObject.affectsConfiguration('editor', { resource: URI.file('file1'), overrideIdentifier: 'markdown' }));
assert.ok(testObject.affectsConfiguration('editor', { resource: URI.file('file1'), overrideIdentifier: 'typescript' }));
assert.ok(testObject.affectsConfiguration('editor', { resource: URI.file('file2'), overrideIdentifier: 'json' }));
assert.ok(testObject.affectsConfiguration('editor', { resource: URI.file('file2'), overrideIdentifier: 'markdown' }));
assert.ok(testObject.affectsConfiguration('editor', { resource: URI.file('file2'), overrideIdentifier: 'typescript' }));
assert.ok(testObject.affectsConfiguration('editor.lineNumbers'));
assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { resource: URI.file('file1') }));
assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { resource: URI.file('file2') }));
assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { resource: URI.file('file1'), overrideIdentifier: 'json' }));
assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { resource: URI.file('file1'), overrideIdentifier: 'markdown' }));
assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { resource: URI.file('file1'), overrideIdentifier: 'typescript' }));
assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { resource: URI.file('file2'), overrideIdentifier: 'json' }));
assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { resource: URI.file('file2'), overrideIdentifier: 'markdown' }));
assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { resource: URI.file('file2'), overrideIdentifier: 'typescript' }));
assert.ok(!testObject.affectsConfiguration('editor.wordWrap'));
assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { resource: URI.file('file1') }));
assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { resource: URI.file('file2') }));
assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { resource: URI.file('file1'), overrideIdentifier: 'json' }));
assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { resource: URI.file('file1'), overrideIdentifier: 'markdown' }));
assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { resource: URI.file('file1'), overrideIdentifier: 'typescript' }));
assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { resource: URI.file('file2'), overrideIdentifier: 'json' }));
assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { resource: URI.file('file2'), overrideIdentifier: 'markdown' }));
assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { resource: URI.file('file2'), overrideIdentifier: 'typescript' }));
assert.ok(!testObject.affectsConfiguration('editor.fontSize'));
assert.ok(!testObject.affectsConfiguration('editor.fontSize', { resource: URI.file('file1') }));
assert.ok(!testObject.affectsConfiguration('editor.fontSize', { resource: URI.file('file2') }));
});
});
function toConfigurationModel(obj: any): ConfigurationModel {
const parser = new ConfigurationModelParser('test');
parser.parseContent(JSON.stringify(obj));
return parser.configurationModel;
}

View File

@@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Registry } from 'vs/platform/registry/common/platform';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
suite('ConfigurationRegistry', () => {
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
test('configuration override', async () => {
configurationRegistry.registerConfiguration({
'id': '_test_default',
'type': 'object',
'properties': {
'config': {
'type': 'object',
}
}
});
configurationRegistry.registerDefaultConfigurations([{ 'config': { a: 1, b: 2 } }]);
configurationRegistry.registerDefaultConfigurations([{ '[lang]': { a: 2, c: 3 } }]);
assert.deepEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 1, b: 2 });
assert.deepEqual(configurationRegistry.getConfigurationProperties()['[lang]'].default, { a: 2, c: 3 });
});
test('configuration override defaults - merges defaults', async () => {
configurationRegistry.registerDefaultConfigurations([{ '[lang]': { a: 1, b: 2 } }]);
configurationRegistry.registerDefaultConfigurations([{ '[lang]': { a: 2, c: 3 } }]);
assert.deepEqual(configurationRegistry.getConfigurationProperties()['[lang]'].default, { a: 2, b: 2, c: 3 });
});
test('configuration defaults - overrides defaults', async () => {
configurationRegistry.registerConfiguration({
'id': '_test_default',
'type': 'object',
'properties': {
'config': {
'type': 'object',
}
}
});
configurationRegistry.registerDefaultConfigurations([{ 'config': { a: 1, b: 2 } }]);
configurationRegistry.registerDefaultConfigurations([{ 'config': { a: 2, c: 3 } }]);
assert.deepEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, c: 3 });
});
});

View File

@@ -0,0 +1,246 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Registry } from 'vs/platform/registry/common/platform';
import { ConfigurationService } from 'vs/platform/configuration/common/configurationService';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
import { URI } from 'vs/base/common/uri';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { Event } from 'vs/base/common/event';
import { NullLogService } from 'vs/platform/log/common/log';
import { FileService } from 'vs/platform/files/common/fileService';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { IFileService } from 'vs/platform/files/common/files';
import { VSBuffer } from 'vs/base/common/buffer';
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
suite('ConfigurationService', () => {
let fileService: IFileService;
let settingsResource: URI;
const disposables: DisposableStore = new DisposableStore();
setup(async () => {
fileService = disposables.add(new FileService(new NullLogService()));
const diskFileSystemProvider = disposables.add(new InMemoryFileSystemProvider());
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
settingsResource = URI.file('settings.json');
});
teardown(() => disposables.clear());
test('simple', async () => {
await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "foo": "bar" }'));
const testObject = disposables.add(new ConfigurationService(settingsResource, fileService));
await testObject.initialize();
const config = testObject.getValue<{
foo: string;
}>();
assert.ok(config);
assert.equal(config.foo, 'bar');
});
test('config gets flattened', async () => {
await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "testworkbench.editor.tabs": true }'));
const testObject = disposables.add(new ConfigurationService(settingsResource, fileService));
await testObject.initialize();
const config = testObject.getValue<{
testworkbench: {
editor: {
tabs: boolean;
};
};
}>();
assert.ok(config);
assert.ok(config.testworkbench);
assert.ok(config.testworkbench.editor);
assert.equal(config.testworkbench.editor.tabs, true);
});
test('error case does not explode', async () => {
await fileService.writeFile(settingsResource, VSBuffer.fromString(',,,,'));
const testObject = disposables.add(new ConfigurationService(settingsResource, fileService));
await testObject.initialize();
const config = testObject.getValue<{
foo: string;
}>();
assert.ok(config);
});
test('missing file does not explode', async () => {
const testObject = disposables.add(new ConfigurationService(URI.file('__testFile'), fileService));
await testObject.initialize();
const config = testObject.getValue<{ foo: string }>();
assert.ok(config);
});
test('trigger configuration change event when file does not exist', async () => {
const testObject = disposables.add(new ConfigurationService(settingsResource, fileService));
await testObject.initialize();
return new Promise<void>(async (c) => {
disposables.add(Event.filter(testObject.onDidChangeConfiguration, e => e.source === ConfigurationTarget.USER)(() => {
assert.equal(testObject.getValue('foo'), 'bar');
c();
}));
await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "foo": "bar" }'));
});
});
test('trigger configuration change event when file exists', async () => {
const testObject = disposables.add(new ConfigurationService(settingsResource, fileService));
await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "foo": "bar" }'));
await testObject.initialize();
return new Promise<void>((c) => {
disposables.add(Event.filter(testObject.onDidChangeConfiguration, e => e.source === ConfigurationTarget.USER)(async (e) => {
assert.equal(testObject.getValue('foo'), 'barz');
c();
}));
fileService.writeFile(settingsResource, VSBuffer.fromString('{ "foo": "barz" }'));
});
});
test('reloadConfiguration', async () => {
await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "foo": "bar" }'));
const testObject = disposables.add(new ConfigurationService(settingsResource, fileService));
await testObject.initialize();
let config = testObject.getValue<{
foo: string;
}>();
assert.ok(config);
assert.equal(config.foo, 'bar');
await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "foo": "changed" }'));
// force a reload to get latest
await testObject.reloadConfiguration();
config = testObject.getValue<{
foo: string;
}>();
assert.ok(config);
assert.equal(config.foo, 'changed');
});
test('model defaults', async () => {
interface ITestSetting {
configuration: {
service: {
testSetting: string;
}
};
}
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
'id': '_test',
'type': 'object',
'properties': {
'configuration.service.testSetting': {
'type': 'string',
'default': 'isSet'
}
}
});
let testObject = disposables.add(new ConfigurationService(URI.file('__testFile'), fileService));
await testObject.initialize();
let setting = testObject.getValue<ITestSetting>();
assert.ok(setting);
assert.equal(setting.configuration.service.testSetting, 'isSet');
await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "testworkbench.editor.tabs": true }'));
testObject = disposables.add(new ConfigurationService(settingsResource, fileService));
setting = testObject.getValue<ITestSetting>();
assert.ok(setting);
assert.equal(setting.configuration.service.testSetting, 'isSet');
await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "configuration.service.testSetting": "isChanged" }'));
await testObject.reloadConfiguration();
setting = testObject.getValue<ITestSetting>();
assert.ok(setting);
assert.equal(setting.configuration.service.testSetting, 'isChanged');
});
test('lookup', async () => {
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
'id': '_test',
'type': 'object',
'properties': {
'lookup.service.testSetting': {
'type': 'string',
'default': 'isSet'
}
}
});
const testObject = disposables.add(new ConfigurationService(settingsResource, fileService));
testObject.initialize();
let res = testObject.inspect('something.missing');
assert.strictEqual(res.value, undefined);
assert.strictEqual(res.defaultValue, undefined);
assert.strictEqual(res.userValue, undefined);
res = testObject.inspect('lookup.service.testSetting');
assert.strictEqual(res.defaultValue, 'isSet');
assert.strictEqual(res.value, 'isSet');
assert.strictEqual(res.userValue, undefined);
await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "lookup.service.testSetting": "bar" }'));
await testObject.reloadConfiguration();
res = testObject.inspect('lookup.service.testSetting');
assert.strictEqual(res.defaultValue, 'isSet');
assert.strictEqual(res.userValue, 'bar');
assert.strictEqual(res.value, 'bar');
});
test('lookup with null', async () => {
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
'id': '_testNull',
'type': 'object',
'properties': {
'lookup.service.testNullSetting': {
'type': 'null',
}
}
});
const testObject = disposables.add(new ConfigurationService(settingsResource, fileService));
testObject.initialize();
let res = testObject.inspect('lookup.service.testNullSetting');
assert.strictEqual(res.defaultValue, null);
assert.strictEqual(res.value, null);
assert.strictEqual(res.userValue, undefined);
await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "lookup.service.testNullSetting": null }'));
await testObject.reloadConfiguration();
res = testObject.inspect('lookup.service.testNullSetting');
assert.strictEqual(res.defaultValue, null);
assert.strictEqual(res.value, null);
assert.strictEqual(res.userValue, null);
});
});

View File

@@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TernarySearchTree } from 'vs/base/common/map';
import { URI } from 'vs/base/common/uri';
import { getConfigurationKeys, IConfigurationOverrides, IConfigurationService, getConfigurationValue, isConfigurationOverrides, IConfigurationValue } from 'vs/platform/configuration/common/configuration';
import { Emitter } from 'vs/base/common/event';
export class TestConfigurationService implements IConfigurationService {
public _serviceBrand: undefined;
private configuration: any;
readonly onDidChangeConfiguration = new Emitter<any>().event;
constructor(configuration?: any) {
this.configuration = configuration || Object.create(null);
}
private configurationByRoot: TernarySearchTree<string, any> = TernarySearchTree.forPaths<any>();
public reloadConfiguration<T>(): Promise<T> {
return Promise.resolve(this.getValue());
}
public getValue(arg1?: any, arg2?: any): any {
let configuration;
const overrides = isConfigurationOverrides(arg1) ? arg1 : isConfigurationOverrides(arg2) ? arg2 : undefined;
if (overrides) {
if (overrides.resource) {
configuration = this.configurationByRoot.findSubstr(overrides.resource.fsPath);
}
}
configuration = configuration ? configuration : this.configuration;
if (arg1 && typeof arg1 === 'string') {
return getConfigurationValue(configuration, arg1);
}
return configuration;
}
public updateValue(key: string, value: any): Promise<void> {
return Promise.resolve(undefined);
}
public setUserConfiguration(key: any, value: any, root?: URI): Promise<void> {
if (root) {
const configForRoot = this.configurationByRoot.get(root.fsPath) || Object.create(null);
configForRoot[key] = value;
this.configurationByRoot.set(root.fsPath, configForRoot);
} else {
this.configuration[key] = value;
}
return Promise.resolve(undefined);
}
public inspect<T>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<T> {
const config = this.getValue(undefined, overrides);
return {
value: getConfigurationValue<T>(config, key),
defaultValue: getConfigurationValue<T>(config, key),
userValue: getConfigurationValue<T>(config, key)
};
}
public keys() {
return {
default: getConfigurationKeys(),
user: Object.keys(this.configuration),
workspace: [],
workspaceFolder: []
};
}
public getConfigurationData() {
return null;
}
}

View File

@@ -0,0 +1,493 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event';
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { distinct } from 'vs/base/common/objects';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContext, IContextKey, IContextKeyChangeEvent, IContextKeyService, IContextKeyServiceTarget, IReadableSet, SET_CONTEXT_COMMAND_ID, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver';
const KEYBINDING_CONTEXT_ATTR = 'data-keybinding-context';
export class Context implements IContext {
protected _parent: Context | null;
protected _value: { [key: string]: any; };
protected _id: number;
constructor(id: number, parent: Context | null) {
this._id = id;
this._parent = parent;
this._value = Object.create(null);
this._value['_contextId'] = id;
}
public setValue(key: string, value: any): boolean {
// console.log('SET ' + key + ' = ' + value + ' ON ' + this._id);
if (this._value[key] !== value) {
this._value[key] = value;
return true;
}
return false;
}
public removeValue(key: string): boolean {
// console.log('REMOVE ' + key + ' FROM ' + this._id);
if (key in this._value) {
delete this._value[key];
return true;
}
return false;
}
public getValue<T>(key: string): T | undefined {
const ret = this._value[key];
if (typeof ret === 'undefined' && this._parent) {
return this._parent.getValue<T>(key);
}
return ret;
}
public updateParent(parent: Context): void {
this._parent = parent;
}
public collectAllValues(): { [key: string]: any; } {
let result = this._parent ? this._parent.collectAllValues() : Object.create(null);
result = { ...result, ...this._value };
delete result['_contextId'];
return result;
}
}
class NullContext extends Context {
static readonly INSTANCE = new NullContext();
constructor() {
super(-1, null);
}
public setValue(key: string, value: any): boolean {
return false;
}
public removeValue(key: string): boolean {
return false;
}
public getValue<T>(key: string): T | undefined {
return undefined;
}
collectAllValues(): { [key: string]: any; } {
return Object.create(null);
}
}
class ConfigAwareContextValuesContainer extends Context {
private static readonly _keyPrefix = 'config.';
private readonly _values = new Map<string, any>();
private readonly _listener: IDisposable;
constructor(
id: number,
private readonly _configurationService: IConfigurationService,
emitter: Emitter<IContextKeyChangeEvent>
) {
super(id, null);
this._listener = this._configurationService.onDidChangeConfiguration(event => {
if (event.source === ConfigurationTarget.DEFAULT) {
// new setting, reset everything
const allKeys = Array.from(this._values.keys());
this._values.clear();
emitter.fire(new ArrayContextKeyChangeEvent(allKeys));
} else {
const changedKeys: string[] = [];
for (const configKey of event.affectedKeys) {
const contextKey = `config.${configKey}`;
if (this._values.has(contextKey)) {
this._values.delete(contextKey);
changedKeys.push(contextKey);
}
}
emitter.fire(new ArrayContextKeyChangeEvent(changedKeys));
}
});
}
dispose(): void {
this._listener.dispose();
}
getValue(key: string): any {
if (key.indexOf(ConfigAwareContextValuesContainer._keyPrefix) !== 0) {
return super.getValue(key);
}
if (this._values.has(key)) {
return this._values.get(key);
}
const configKey = key.substr(ConfigAwareContextValuesContainer._keyPrefix.length);
const configValue = this._configurationService.getValue(configKey);
let value: any = undefined;
switch (typeof configValue) {
case 'number':
case 'boolean':
case 'string':
value = configValue;
break;
default:
if (Array.isArray(configValue)) {
value = JSON.stringify(configValue);
}
}
this._values.set(key, value);
return value;
}
setValue(key: string, value: any): boolean {
return super.setValue(key, value);
}
removeValue(key: string): boolean {
return super.removeValue(key);
}
collectAllValues(): { [key: string]: any; } {
const result: { [key: string]: any } = Object.create(null);
this._values.forEach((value, index) => result[index] = value);
return { ...result, ...super.collectAllValues() };
}
}
class ContextKey<T> implements IContextKey<T> {
private _service: AbstractContextKeyService;
private _key: string;
private _defaultValue: T | undefined;
constructor(service: AbstractContextKeyService, key: string, defaultValue: T | undefined) {
this._service = service;
this._key = key;
this._defaultValue = defaultValue;
this.reset();
}
public set(value: T): void {
this._service.setContext(this._key, value);
}
public reset(): void {
if (typeof this._defaultValue === 'undefined') {
this._service.removeContext(this._key);
} else {
this._service.setContext(this._key, this._defaultValue);
}
}
public get(): T | undefined {
return this._service.getContextKeyValue<T>(this._key);
}
}
class SimpleContextKeyChangeEvent implements IContextKeyChangeEvent {
constructor(readonly key: string) { }
affectsSome(keys: IReadableSet<string>): boolean {
return keys.has(this.key);
}
}
class ArrayContextKeyChangeEvent implements IContextKeyChangeEvent {
constructor(readonly keys: string[]) { }
affectsSome(keys: IReadableSet<string>): boolean {
for (const key of this.keys) {
if (keys.has(key)) {
return true;
}
}
return false;
}
}
class CompositeContextKeyChangeEvent implements IContextKeyChangeEvent {
constructor(readonly events: IContextKeyChangeEvent[]) { }
affectsSome(keys: IReadableSet<string>): boolean {
for (const e of this.events) {
if (e.affectsSome(keys)) {
return true;
}
}
return false;
}
}
export abstract class AbstractContextKeyService implements IContextKeyService {
public _serviceBrand: undefined;
protected _isDisposed: boolean;
protected _onDidChangeContext = new PauseableEmitter<IContextKeyChangeEvent>({ merge: input => new CompositeContextKeyChangeEvent(input) });
protected _myContextId: number;
constructor(myContextId: number) {
this._isDisposed = false;
this._myContextId = myContextId;
}
public get contextId(): number {
return this._myContextId;
}
abstract dispose(): void;
public createKey<T>(key: string, defaultValue: T | undefined): IContextKey<T> {
if (this._isDisposed) {
throw new Error(`AbstractContextKeyService has been disposed`);
}
return new ContextKey(this, key, defaultValue);
}
public get onDidChangeContext(): Event<IContextKeyChangeEvent> {
return this._onDidChangeContext.event;
}
bufferChangeEvents(callback: Function): void {
this._onDidChangeContext.pause();
try {
callback();
} finally {
this._onDidChangeContext.resume();
}
}
public createScoped(domNode: IContextKeyServiceTarget): IContextKeyService {
if (this._isDisposed) {
throw new Error(`AbstractContextKeyService has been disposed`);
}
return new ScopedContextKeyService(this, domNode);
}
public contextMatchesRules(rules: ContextKeyExpression | undefined): boolean {
if (this._isDisposed) {
throw new Error(`AbstractContextKeyService has been disposed`);
}
const context = this.getContextValuesContainer(this._myContextId);
const result = KeybindingResolver.contextMatchesRules(context, rules);
// console.group(rules.serialize() + ' -> ' + result);
// rules.keys().forEach(key => { console.log(key, ctx[key]); });
// console.groupEnd();
return result;
}
public getContextKeyValue<T>(key: string): T | undefined {
if (this._isDisposed) {
return undefined;
}
return this.getContextValuesContainer(this._myContextId).getValue<T>(key);
}
public setContext(key: string, value: any): void {
if (this._isDisposed) {
return;
}
const myContext = this.getContextValuesContainer(this._myContextId);
if (!myContext) {
return;
}
if (myContext.setValue(key, value)) {
this._onDidChangeContext.fire(new SimpleContextKeyChangeEvent(key));
}
}
public removeContext(key: string): void {
if (this._isDisposed) {
return;
}
if (this.getContextValuesContainer(this._myContextId).removeValue(key)) {
this._onDidChangeContext.fire(new SimpleContextKeyChangeEvent(key));
}
}
public getContext(target: IContextKeyServiceTarget | null): IContext {
if (this._isDisposed) {
return NullContext.INSTANCE;
}
return this.getContextValuesContainer(findContextAttr(target));
}
public abstract getContextValuesContainer(contextId: number): Context;
public abstract createChildContext(parentContextId?: number): number;
public abstract disposeContext(contextId: number): void;
public abstract updateParent(parentContextKeyService?: IContextKeyService): void;
}
export class ContextKeyService extends AbstractContextKeyService implements IContextKeyService {
private _lastContextId: number;
private readonly _contexts = new Map<number, Context>();
private readonly _toDispose = new DisposableStore();
constructor(@IConfigurationService configurationService: IConfigurationService) {
super(0);
this._lastContextId = 0;
const myContext = new ConfigAwareContextValuesContainer(this._myContextId, configurationService, this._onDidChangeContext);
this._contexts.set(this._myContextId, myContext);
this._toDispose.add(myContext);
// Uncomment this to see the contexts continuously logged
// let lastLoggedValue: string | null = null;
// setInterval(() => {
// let values = Object.keys(this._contexts).map((key) => this._contexts[key]);
// let logValue = values.map(v => JSON.stringify(v._value, null, '\t')).join('\n');
// if (lastLoggedValue !== logValue) {
// lastLoggedValue = logValue;
// console.log(lastLoggedValue);
// }
// }, 2000);
}
public dispose(): void {
this._isDisposed = true;
this._toDispose.dispose();
}
public getContextValuesContainer(contextId: number): Context {
if (this._isDisposed) {
return NullContext.INSTANCE;
}
return this._contexts.get(contextId) || NullContext.INSTANCE;
}
public createChildContext(parentContextId: number = this._myContextId): number {
if (this._isDisposed) {
throw new Error(`ContextKeyService has been disposed`);
}
let id = (++this._lastContextId);
this._contexts.set(id, new Context(id, this.getContextValuesContainer(parentContextId)));
return id;
}
public disposeContext(contextId: number): void {
if (!this._isDisposed) {
this._contexts.delete(contextId);
}
}
public updateParent(_parentContextKeyService: IContextKeyService): void {
throw new Error('Cannot update parent of root ContextKeyService');
}
}
class ScopedContextKeyService extends AbstractContextKeyService {
private _parent: AbstractContextKeyService;
private _domNode: IContextKeyServiceTarget | undefined;
private _parentChangeListener: IDisposable | undefined;
constructor(parent: AbstractContextKeyService, domNode?: IContextKeyServiceTarget) {
super(parent.createChildContext());
this._parent = parent;
this.updateParentChangeListener();
if (domNode) {
this._domNode = domNode;
this._domNode.setAttribute(KEYBINDING_CONTEXT_ATTR, String(this._myContextId));
}
}
private updateParentChangeListener(): void {
if (this._parentChangeListener) {
this._parentChangeListener.dispose();
}
this._parentChangeListener = this._parent.onDidChangeContext(e => {
// Forward parent events to this listener. Parent will change.
this._onDidChangeContext.fire(e);
});
}
public dispose(): void {
this._isDisposed = true;
this._parent.disposeContext(this._myContextId);
this._parentChangeListener?.dispose();
if (this._domNode) {
this._domNode.removeAttribute(KEYBINDING_CONTEXT_ATTR);
this._domNode = undefined;
}
}
public get onDidChangeContext(): Event<IContextKeyChangeEvent> {
return this._onDidChangeContext.event;
}
public getContextValuesContainer(contextId: number): Context {
if (this._isDisposed) {
return NullContext.INSTANCE;
}
return this._parent.getContextValuesContainer(contextId);
}
public createChildContext(parentContextId: number = this._myContextId): number {
if (this._isDisposed) {
throw new Error(`ScopedContextKeyService has been disposed`);
}
return this._parent.createChildContext(parentContextId);
}
public disposeContext(contextId: number): void {
if (this._isDisposed) {
return;
}
this._parent.disposeContext(contextId);
}
public updateParent(parentContextKeyService: AbstractContextKeyService): void {
const thisContainer = this._parent.getContextValuesContainer(this._myContextId);
const oldAllValues = thisContainer.collectAllValues();
this._parent = parentContextKeyService;
this.updateParentChangeListener();
const newParentContainer = this._parent.getContextValuesContainer(this._parent.contextId);
thisContainer.updateParent(newParentContainer);
const newAllValues = thisContainer.collectAllValues();
const allValuesDiff = {
...distinct(oldAllValues, newAllValues),
...distinct(newAllValues, oldAllValues)
};
const changedKeys = Object.keys(allValuesDiff);
this._onDidChangeContext.fire(new ArrayContextKeyChangeEvent(changedKeys));
}
}
function findContextAttr(domNode: IContextKeyServiceTarget | null): number {
while (domNode) {
if (domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) {
const attr = domNode.getAttribute(KEYBINDING_CONTEXT_ATTR);
if (attr) {
return parseInt(attr, 10);
}
return NaN;
}
domNode = domNode.parentElement;
}
return 0;
}
CommandsRegistry.registerCommand(SET_CONTEXT_COMMAND_ID, function (accessor, contextKey: any, contextValue: any) {
accessor.get(IContextKeyService).createKey(String(contextKey), contextValue);
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { isMacintosh, isLinux, isWindows, isWeb } from 'vs/base/common/platform';
export const IsMacContext = new RawContextKey<boolean>('isMac', isMacintosh);
export const IsLinuxContext = new RawContextKey<boolean>('isLinux', isLinux);
export const IsWindowsContext = new RawContextKey<boolean>('isWindows', isWindows);
export const IsWebContext = new RawContextKey<boolean>('isWeb', isWeb);
export const IsMacNativeContext = new RawContextKey<boolean>('isMacNative', isMacintosh && !isWeb);
export const IsDevelopmentContext = new RawContextKey<boolean>('isDevelopment', false);
export const InputFocusedContextKey = 'inputFocus';
export const InputFocusedContext = new RawContextKey<boolean>(InputFocusedContextKey, false);

View File

@@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService';
import * as assert from 'assert';
suite('ContextKeyService', () => {
test('updateParent', () => {
const root = new ContextKeyService(new TestConfigurationService());
const parent1 = root.createScoped(document.createElement('div'));
const parent2 = root.createScoped(document.createElement('div'));
const child = parent1.createScoped(document.createElement('div'));
parent1.createKey('testA', 1);
parent1.createKey('testB', 2);
parent1.createKey('testD', 0);
parent2.createKey('testA', 3);
parent2.createKey('testC', 4);
parent2.createKey('testD', 0);
let complete: () => void;
let reject: (err: Error) => void;
const p = new Promise<void>((_complete, _reject) => {
complete = _complete;
reject = _reject;
});
child.onDidChangeContext(e => {
try {
assert.ok(e.affectsSome(new Set(['testA'])), 'testA changed');
assert.ok(e.affectsSome(new Set(['testB'])), 'testB changed');
assert.ok(e.affectsSome(new Set(['testC'])), 'testC changed');
assert.ok(!e.affectsSome(new Set(['testD'])), 'testD did not change');
assert.equal(child.getContextKeyValue('testA'), 3);
assert.equal(child.getContextKeyValue('testB'), undefined);
assert.equal(child.getContextKeyValue('testC'), 4);
assert.equal(child.getContextKeyValue('testD'), 0);
} catch (err) {
reject(err);
return;
}
complete();
});
child.updateParent(parent2);
return p;
});
});

View File

@@ -0,0 +1,189 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { isMacintosh, isLinux, isWindows } from 'vs/base/common/platform';
function createContext(ctx: any) {
return {
getValue: (key: string) => {
return ctx[key];
}
};
}
suite('ContextKeyExpr', () => {
test('ContextKeyExpr.equals', () => {
let a = ContextKeyExpr.and(
ContextKeyExpr.has('a1'),
ContextKeyExpr.and(ContextKeyExpr.has('and.a')),
ContextKeyExpr.has('a2'),
ContextKeyExpr.regex('d3', /d.*/),
ContextKeyExpr.regex('d4', /\*\*3*/),
ContextKeyExpr.equals('b1', 'bb1'),
ContextKeyExpr.equals('b2', 'bb2'),
ContextKeyExpr.notEquals('c1', 'cc1'),
ContextKeyExpr.notEquals('c2', 'cc2'),
ContextKeyExpr.not('d1'),
ContextKeyExpr.not('d2')
)!;
let b = ContextKeyExpr.and(
ContextKeyExpr.equals('b2', 'bb2'),
ContextKeyExpr.notEquals('c1', 'cc1'),
ContextKeyExpr.not('d1'),
ContextKeyExpr.regex('d4', /\*\*3*/),
ContextKeyExpr.notEquals('c2', 'cc2'),
ContextKeyExpr.has('a2'),
ContextKeyExpr.equals('b1', 'bb1'),
ContextKeyExpr.regex('d3', /d.*/),
ContextKeyExpr.has('a1'),
ContextKeyExpr.and(ContextKeyExpr.equals('and.a', true)),
ContextKeyExpr.not('d2')
)!;
assert(a.equals(b), 'expressions should be equal');
});
test('normalize', () => {
let key1IsTrue = ContextKeyExpr.equals('key1', true);
let key1IsNotFalse = ContextKeyExpr.notEquals('key1', false);
let key1IsFalse = ContextKeyExpr.equals('key1', false);
let key1IsNotTrue = ContextKeyExpr.notEquals('key1', true);
assert.ok(key1IsTrue.equals(ContextKeyExpr.has('key1')));
assert.ok(key1IsNotFalse.equals(ContextKeyExpr.has('key1')));
assert.ok(key1IsFalse.equals(ContextKeyExpr.not('key1')));
assert.ok(key1IsNotTrue.equals(ContextKeyExpr.not('key1')));
});
test('evaluate', () => {
let context = createContext({
'a': true,
'b': false,
'c': '5',
'd': 'd'
});
function testExpression(expr: string, expected: boolean): void {
// console.log(expr + ' ' + expected);
let rules = ContextKeyExpr.deserialize(expr);
assert.equal(rules!.evaluate(context), expected, expr);
}
function testBatch(expr: string, value: any): void {
/* eslint-disable eqeqeq */
testExpression(expr, !!value);
testExpression(expr + ' == true', !!value);
testExpression(expr + ' != true', !value);
testExpression(expr + ' == false', !value);
testExpression(expr + ' != false', !!value);
testExpression(expr + ' == 5', value == <any>'5');
testExpression(expr + ' != 5', value != <any>'5');
testExpression('!' + expr, !value);
testExpression(expr + ' =~ /d.*/', /d.*/.test(value));
testExpression(expr + ' =~ /D/i', /D/i.test(value));
/* eslint-enable eqeqeq */
}
testBatch('a', true);
testBatch('b', false);
testBatch('c', '5');
testBatch('d', 'd');
testBatch('z', undefined);
testExpression('true', true);
testExpression('false', false);
testExpression('a && !b', true && !false);
testExpression('a && b', true && false);
testExpression('a && !b && c == 5', true && !false && '5' === '5');
testExpression('d =~ /e.*/', false);
// precedence test: false && true || true === true because && is evaluated first
testExpression('b && a || a', true);
testExpression('a || b', true);
testExpression('b || b', false);
testExpression('b && a || a && b', false);
});
test('negate', () => {
function testNegate(expr: string, expected: string): void {
const actual = ContextKeyExpr.deserialize(expr)!.negate().serialize();
assert.strictEqual(actual, expected);
}
testNegate('true', 'false');
testNegate('false', 'true');
testNegate('a', '!a');
testNegate('a && b || c', '!a && !c || !b && !c');
testNegate('a && b || c || d', '!a && !c && !d || !b && !c && !d');
testNegate('!a && !b || !c && !d', 'a && c || a && d || b && c || b && d');
testNegate('!a && !b || !c && !d || !e && !f', 'a && c && e || a && c && f || a && d && e || a && d && f || b && c && e || b && c && f || b && d && e || b && d && f');
});
test('false, true', () => {
function testNormalize(expr: string, expected: string): void {
const actual = ContextKeyExpr.deserialize(expr)!.serialize();
assert.strictEqual(actual, expected);
}
testNormalize('true', 'true');
testNormalize('!true', 'false');
testNormalize('false', 'false');
testNormalize('!false', 'true');
testNormalize('a && true', 'a');
testNormalize('a && false', 'false');
testNormalize('a || true', 'true');
testNormalize('a || false', 'a');
testNormalize('isMac', isMacintosh ? 'true' : 'false');
testNormalize('isLinux', isLinux ? 'true' : 'false');
testNormalize('isWindows', isWindows ? 'true' : 'false');
});
test('issue #101015: distribute OR', () => {
function t(expr1: string, expr2: string, expected: string | undefined): void {
const e1 = ContextKeyExpr.deserialize(expr1);
const e2 = ContextKeyExpr.deserialize(expr2);
const actual = ContextKeyExpr.and(e1, e2)?.serialize();
assert.strictEqual(actual, expected);
}
t('a', 'b', 'a && b');
t('a || b', 'c', 'a && c || b && c');
t('a || b', 'c || d', 'a && c || a && d || b && c || b && d');
t('a || b', 'c && d', 'a && c && d || b && c && d');
t('a || b', 'c && d || e', 'a && e || b && e || a && c && d || b && c && d');
});
test('ContextKeyInExpr', () => {
const ainb = ContextKeyExpr.deserialize('a in b')!;
assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [3, 2, 1] })), true);
assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2, 3] })), true);
assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2] })), false);
assert.equal(ainb.evaluate(createContext({ 'a': 3 })), false);
assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': null })), false);
assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': ['x'] })), true);
assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': ['y'] })), false);
assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': {} })), false);
assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': false } })), true);
assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': true } })), true);
assert.equal(ainb.evaluate(createContext({ 'a': 'prototype', 'b': {} })), false);
});
test('issue #106524: distributing AND should normalize', () => {
const actual = ContextKeyExpr.and(
ContextKeyExpr.or(
ContextKeyExpr.has('a'),
ContextKeyExpr.has('b')
),
ContextKeyExpr.has('c')
);
const expected = ContextKeyExpr.or(
ContextKeyExpr.and(
ContextKeyExpr.has('a'),
ContextKeyExpr.has('c')
),
ContextKeyExpr.and(
ContextKeyExpr.has('b'),
ContextKeyExpr.has('c')
)
);
assert.equal(actual!.equals(expected!), true);
});
});

View File

@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.context-view .monaco-menu {
min-width: 130px;
}

View File

@@ -0,0 +1,164 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./contextMenuHandler';
import { ActionRunner, IRunEvent, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions';
import { combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { Menu } from 'vs/base/browser/ui/menu/menu';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IContextMenuDelegate } from 'vs/base/browser/contextmenu';
import { EventType, $, isHTMLElement } from 'vs/base/browser/dom';
import { attachMenuStyler } from 'vs/platform/theme/common/styler';
import { domEvent } from 'vs/base/browser/event';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
export interface IContextMenuHandlerOptions {
blockMouse: boolean;
}
export class ContextMenuHandler {
private focusToReturn: HTMLElement | null = null;
private block: HTMLElement | null = null;
private options: IContextMenuHandlerOptions = { blockMouse: true };
constructor(
private contextViewService: IContextViewService,
private telemetryService: ITelemetryService,
private notificationService: INotificationService,
private keybindingService: IKeybindingService,
private themeService: IThemeService
) { }
configure(options: IContextMenuHandlerOptions): void {
this.options = options;
}
showContextMenu(delegate: IContextMenuDelegate): void {
const actions = delegate.getActions();
if (!actions.length) {
return; // Don't render an empty context menu
}
this.focusToReturn = document.activeElement as HTMLElement;
let menu: Menu | undefined;
let shadowRootElement = isHTMLElement(delegate.domForShadowRoot) ? delegate.domForShadowRoot : undefined;
this.contextViewService.showContextView({
getAnchor: () => delegate.getAnchor(),
canRelayout: false,
anchorAlignment: delegate.anchorAlignment,
render: (container) => {
let className = delegate.getMenuClassName ? delegate.getMenuClassName() : '';
if (className) {
container.className += ' ' + className;
}
// Render invisible div to block mouse interaction in the rest of the UI
if (this.options.blockMouse) {
this.block = container.appendChild($('.context-view-block'));
this.block.style.position = 'fixed';
this.block.style.cursor = 'initial';
this.block.style.left = '0';
this.block.style.top = '0';
this.block.style.width = '100%';
this.block.style.height = '100%';
this.block.style.zIndex = '-1';
domEvent(this.block, EventType.MOUSE_DOWN)((e: MouseEvent) => e.stopPropagation());
}
const menuDisposables = new DisposableStore();
const actionRunner = delegate.actionRunner || new ActionRunner();
actionRunner.onDidBeforeRun(this.onActionRun, this, menuDisposables);
actionRunner.onDidRun(this.onDidActionRun, this, menuDisposables);
menu = new Menu(container, actions, {
actionViewItemProvider: delegate.getActionViewItem,
context: delegate.getActionsContext ? delegate.getActionsContext() : null,
actionRunner,
getKeyBinding: delegate.getKeyBinding ? delegate.getKeyBinding : action => this.keybindingService.lookupKeybinding(action.id)
});
menuDisposables.add(attachMenuStyler(menu, this.themeService));
menu.onDidCancel(() => this.contextViewService.hideContextView(true), null, menuDisposables);
menu.onDidBlur(() => this.contextViewService.hideContextView(true), null, menuDisposables);
domEvent(window, EventType.BLUR)(() => { this.contextViewService.hideContextView(true); }, null, menuDisposables);
domEvent(window, EventType.MOUSE_DOWN)((e: MouseEvent) => {
if (e.defaultPrevented) {
return;
}
let event = new StandardMouseEvent(e);
let element: HTMLElement | null = event.target;
// Don't do anything as we are likely creating a context menu
if (event.rightButton) {
return;
}
while (element) {
if (element === container) {
return;
}
element = element.parentElement;
}
this.contextViewService.hideContextView(true);
}, null, menuDisposables);
return combinedDisposable(menuDisposables, menu);
},
focus: () => {
if (menu) {
menu.focus(!!delegate.autoSelectFirstItem);
}
},
onHide: (didCancel?: boolean) => {
if (delegate.onHide) {
delegate.onHide(!!didCancel);
}
if (this.block) {
this.block.remove();
this.block = null;
}
if (this.focusToReturn) {
this.focusToReturn.focus();
}
}
}, shadowRootElement, !!shadowRootElement);
}
private onActionRun(e: IRunEvent): void {
if (this.telemetryService) {
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: e.action.id, from: 'contextMenu' });
}
this.contextViewService.hideContextView(false);
// Restore focus here
if (this.focusToReturn) {
this.focusToReturn.focus();
}
}
private onDidActionRun(e: IRunEvent): void {
if (e.error && this.notificationService) {
this.notificationService.error(e.error);
}
}
}

View File

@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ContextMenuHandler, IContextMenuHandlerOptions } from './contextMenuHandler';
import { IContextViewService, IContextMenuService } from './contextView';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IContextMenuDelegate } from 'vs/base/browser/contextmenu';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { Disposable } from 'vs/base/common/lifecycle';
import { ModifierKeyEmitter } from 'vs/base/browser/dom';
export class ContextMenuService extends Disposable implements IContextMenuService {
declare readonly _serviceBrand: undefined;
private contextMenuHandler: ContextMenuHandler;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@INotificationService notificationService: INotificationService,
@IContextViewService contextViewService: IContextViewService,
@IKeybindingService keybindingService: IKeybindingService,
@IThemeService themeService: IThemeService
) {
super();
this.contextMenuHandler = new ContextMenuHandler(contextViewService, telemetryService, notificationService, keybindingService, themeService);
}
configure(options: IContextMenuHandlerOptions): void {
this.contextMenuHandler.configure(options);
}
// ContextMenu
showContextMenu(delegate: IContextMenuDelegate): void {
this.contextMenuHandler.showContextMenu(delegate);
ModifierKeyEmitter.getInstance().resetKeyStatus();
}
}

View File

@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable } from 'vs/base/common/lifecycle';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IContextMenuDelegate } from 'vs/base/browser/contextmenu';
import { AnchorAlignment, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
export const IContextViewService = createDecorator<IContextViewService>('contextViewService');
export interface IContextViewService extends IContextViewProvider {
readonly _serviceBrand: undefined;
showContextView(delegate: IContextViewDelegate, container?: HTMLElement, shadowRoot?: boolean): IDisposable;
hideContextView(data?: any): void;
getContextViewElement(): HTMLElement;
layout(): void;
anchorAlignment?: AnchorAlignment;
}
export interface IContextViewDelegate {
canRelayout?: boolean; // Default: true
getAnchor(): HTMLElement | { x: number; y: number; width?: number; height?: number; };
render(container: HTMLElement): IDisposable;
onDOMEvent?(e: any, activeElement: HTMLElement): void;
onHide?(data?: any): void;
focus?(): void;
anchorAlignment?: AnchorAlignment;
}
export const IContextMenuService = createDecorator<IContextMenuService>('contextMenuService');
export interface IContextMenuService {
readonly _serviceBrand: undefined;
showContextMenu(delegate: IContextMenuDelegate): void;
}

View File

@@ -0,0 +1,72 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IContextViewService, IContextViewDelegate } from './contextView';
import { ContextView, ContextViewDOMPosition } from 'vs/base/browser/ui/contextview/contextview';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
export class ContextViewService extends Disposable implements IContextViewService {
declare readonly _serviceBrand: undefined;
private currentViewDisposable: IDisposable = Disposable.None;
private contextView: ContextView;
private container: HTMLElement;
constructor(
@ILayoutService readonly layoutService: ILayoutService
) {
super();
this.container = layoutService.container;
this.contextView = this._register(new ContextView(this.container, ContextViewDOMPosition.ABSOLUTE));
this.layout();
this._register(layoutService.onLayout(() => this.layout()));
}
// ContextView
setContainer(container: HTMLElement, domPosition?: ContextViewDOMPosition): void {
this.contextView.setContainer(container, domPosition || ContextViewDOMPosition.ABSOLUTE);
}
showContextView(delegate: IContextViewDelegate, container?: HTMLElement, shadowRoot?: boolean): IDisposable {
if (container) {
if (container !== this.container) {
this.container = container;
this.setContainer(container, shadowRoot ? ContextViewDOMPosition.FIXED_SHADOW : ContextViewDOMPosition.FIXED);
}
} else {
if (this.container !== this.layoutService.container) {
this.container = this.layoutService.container;
this.setContainer(this.container, ContextViewDOMPosition.ABSOLUTE);
}
}
this.contextView.show(delegate);
const disposable = toDisposable(() => {
if (this.currentViewDisposable === disposable) {
this.hideContextView();
}
});
this.currentViewDisposable = disposable;
return disposable;
}
getContextViewElement(): HTMLElement {
return this.contextView.getViewElement();
}
layout(): void {
this.contextView.layout();
}
hideContextView(data?: any): void {
this.contextView.hide(data);
}
}

View File

@@ -0,0 +1,60 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { IRemoteConsoleLog } from 'vs/base/common/console';
import { IProcessEnvironment } from 'vs/base/common/platform';
export const IExtensionHostDebugService = createDecorator<IExtensionHostDebugService>('extensionHostDebugService');
export interface IAttachSessionEvent {
sessionId: string;
subId?: string;
port: number;
}
export interface ILogToSessionEvent {
sessionId: string;
log: IRemoteConsoleLog;
}
export interface ITerminateSessionEvent {
sessionId: string;
subId?: string;
}
export interface IReloadSessionEvent {
sessionId: string;
}
export interface ICloseSessionEvent {
sessionId: string;
}
export interface IOpenExtensionWindowResult {
rendererDebugPort?: number;
}
export interface IExtensionHostDebugService {
readonly _serviceBrand: undefined;
reload(sessionId: string): void;
readonly onReload: Event<IReloadSessionEvent>;
close(sessionId: string): void;
readonly onClose: Event<ICloseSessionEvent>;
attachSession(sessionId: string, port: number, subId?: string): void;
readonly onAttachSession: Event<IAttachSessionEvent>;
logToSession(sessionId: string, log: IRemoteConsoleLog): void;
readonly onLogToSession: Event<ILogToSessionEvent>;
terminateSession(sessionId: string, subId?: string): void;
readonly onTerminateSession: Event<ITerminateSessionEvent>;
openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment, debugRenderer: boolean): Promise<IOpenExtensionWindowResult>;
}

View File

@@ -0,0 +1,107 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IReloadSessionEvent, ICloseSessionEvent, IAttachSessionEvent, ILogToSessionEvent, ITerminateSessionEvent, IExtensionHostDebugService, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug';
import { Event, Emitter } from 'vs/base/common/event';
import { IRemoteConsoleLog } from 'vs/base/common/console';
import { Disposable } from 'vs/base/common/lifecycle';
import { IProcessEnvironment } from 'vs/base/common/platform';
export class ExtensionHostDebugBroadcastChannel<TContext> implements IServerChannel<TContext> {
static readonly ChannelName = 'extensionhostdebugservice';
private readonly _onCloseEmitter = new Emitter<ICloseSessionEvent>();
private readonly _onReloadEmitter = new Emitter<IReloadSessionEvent>();
private readonly _onTerminateEmitter = new Emitter<ITerminateSessionEvent>();
private readonly _onLogToEmitter = new Emitter<ILogToSessionEvent>();
private readonly _onAttachEmitter = new Emitter<IAttachSessionEvent>();
call(ctx: TContext, command: string, arg?: any): Promise<any> {
switch (command) {
case 'close':
return Promise.resolve(this._onCloseEmitter.fire({ sessionId: arg[0] }));
case 'reload':
return Promise.resolve(this._onReloadEmitter.fire({ sessionId: arg[0] }));
case 'terminate':
return Promise.resolve(this._onTerminateEmitter.fire({ sessionId: arg[0] }));
case 'log':
return Promise.resolve(this._onLogToEmitter.fire({ sessionId: arg[0], log: arg[1] }));
case 'attach':
return Promise.resolve(this._onAttachEmitter.fire({ sessionId: arg[0], port: arg[1], subId: arg[2] }));
}
throw new Error('Method not implemented.');
}
listen(ctx: TContext, event: string, arg?: any): Event<any> {
switch (event) {
case 'close':
return this._onCloseEmitter.event;
case 'reload':
return this._onReloadEmitter.event;
case 'terminate':
return this._onTerminateEmitter.event;
case 'log':
return this._onLogToEmitter.event;
case 'attach':
return this._onAttachEmitter.event;
}
throw new Error('Method not implemented.');
}
}
export class ExtensionHostDebugChannelClient extends Disposable implements IExtensionHostDebugService {
declare readonly _serviceBrand: undefined;
constructor(private channel: IChannel) {
super();
}
reload(sessionId: string): void {
this.channel.call('reload', [sessionId]);
}
get onReload(): Event<IReloadSessionEvent> {
return this.channel.listen('reload');
}
close(sessionId: string): void {
this.channel.call('close', [sessionId]);
}
get onClose(): Event<ICloseSessionEvent> {
return this.channel.listen('close');
}
attachSession(sessionId: string, port: number, subId?: string): void {
this.channel.call('attach', [sessionId, port, subId]);
}
get onAttachSession(): Event<IAttachSessionEvent> {
return this.channel.listen('attach');
}
logToSession(sessionId: string, log: IRemoteConsoleLog): void {
this.channel.call('log', [sessionId, log]);
}
get onLogToSession(): Event<ILogToSessionEvent> {
return this.channel.listen('log');
}
terminateSession(sessionId: string, subId?: string): void {
this.channel.call('terminate', [sessionId, subId]);
}
get onTerminateSession(): Event<ITerminateSessionEvent> {
return this.channel.listen('terminate');
}
openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment, debugRenderer: boolean): Promise<IOpenExtensionWindowResult> {
return this.channel.call('openExtensionDevelopmentHostWindow', [args, env, debugRenderer]);
}
}

View File

@@ -0,0 +1,111 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { createServer, AddressInfo } from 'net';
import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc';
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
import { OpenContext } from 'vs/platform/windows/node/window';
export class ElectronExtensionHostDebugBroadcastChannel<TContext> extends ExtensionHostDebugBroadcastChannel<TContext> {
constructor(private windowsMainService: IWindowsMainService) {
super();
}
call(ctx: TContext, command: string, arg?: any): Promise<any> {
if (command === 'openExtensionDevelopmentHostWindow') {
return this.openExtensionDevelopmentHostWindow(arg[0], arg[1], arg[2]);
} else {
return super.call(ctx, command, arg);
}
}
private async openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment, debugRenderer: boolean): Promise<IOpenExtensionWindowResult> {
const pargs = parseArgs(args, OPTIONS);
pargs.debugRenderer = debugRenderer;
const extDevPaths = pargs.extensionDevelopmentPath;
if (!extDevPaths) {
return {};
}
const [codeWindow] = this.windowsMainService.openExtensionDevelopmentHostWindow(extDevPaths, {
context: OpenContext.API,
cli: pargs,
userEnv: Object.keys(env).length > 0 ? env : undefined
});
if (!debugRenderer) {
return {};
}
const debug = codeWindow.win.webContents.debugger;
let listeners = debug.isAttached() ? Infinity : 0;
const server = createServer(listener => {
if (listeners++ === 0) {
debug.attach();
}
let closed = false;
const writeMessage = (message: object) => {
if (!closed) { // in case sendCommand promises settle after closed
listener.write(JSON.stringify(message) + '\0'); // null-delimited, CDP-compatible
}
};
const onMessage = (_event: Event, method: string, params: unknown, sessionId?: string) =>
writeMessage(({ method, params, sessionId }));
codeWindow.win.on('close', () => {
debug.removeListener('message', onMessage);
listener.end();
closed = true;
});
debug.addListener('message', onMessage);
let buf = Buffer.alloc(0);
listener.on('data', data => {
buf = Buffer.concat([buf, data]);
for (let delimiter = buf.indexOf(0); delimiter !== -1; delimiter = buf.indexOf(0)) {
let data: { id: number; sessionId: string; params: {} };
try {
const contents = buf.slice(0, delimiter).toString('utf8');
buf = buf.slice(delimiter + 1);
data = JSON.parse(contents);
} catch (e) {
console.error('error reading cdp line', e);
}
// depends on a new API for which electron.d.ts has not been updated:
// @ts-ignore
debug.sendCommand(data.method, data.params, data.sessionId)
.then((result: object) => writeMessage({ id: data.id, sessionId: data.sessionId, result }))
.catch((error: Error) => writeMessage({ id: data.id, sessionId: data.sessionId, error: { code: 0, message: error.message } }));
}
});
listener.on('error', err => {
console.error('error on cdp pipe:', err);
});
listener.on('close', () => {
closed = true;
if (--listeners === 0) {
debug.detach();
}
});
});
await new Promise<void>(r => server.listen(0, r));
codeWindow.win.on('close', () => server.close());
return { rendererDebugPort: (server.address() as AddressInfo).port };
}
}

View File

@@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { UriComponents } from 'vs/base/common/uri';
import { ProcessItem } from 'vs/base/common/processes';
import { IWorkspace } from 'vs/platform/workspace/common/workspace';
import { IStringDictionary } from 'vs/base/common/collections';
export interface IMachineInfo {
os: string;
cpus?: string;
memory: string;
vmHint: string;
linuxEnv?: ILinuxEnv;
}
export interface ILinuxEnv {
desktopSession?: string;
xdgSessionDesktop?: string;
xdgCurrentDesktop?: string;
xdgSessionType?: string;
}
export interface IDiagnosticInfo {
machineInfo: IMachineInfo;
workspaceMetadata?: IStringDictionary<WorkspaceStats>;
processes?: ProcessItem;
}
export interface SystemInfo extends IMachineInfo {
processArgs: string;
gpuStatus: any;
screenReader: string;
remoteData: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[];
load?: string;
}
export interface IRemoteDiagnosticInfo extends IDiagnosticInfo {
hostName: string;
}
export interface IRemoteDiagnosticError {
hostName: string;
errorMessage: string;
}
export interface IDiagnosticInfoOptions {
includeProcesses?: boolean;
folders?: UriComponents[];
includeExtensions?: boolean;
}
export interface WorkspaceStatItem {
name: string;
count: number;
}
export interface WorkspaceStats {
fileTypes: WorkspaceStatItem[];
configFiles: WorkspaceStatItem[];
fileCount: number;
maxFilesReached: boolean;
launchConfigFiles: WorkspaceStatItem[];
}
export interface PerformanceInfo {
processInfo?: string;
workspaceInfo?: string;
}
export interface IWorkspaceInformation extends IWorkspace {
telemetryId: string | undefined;
rendererSessionId: string;
}
export function isRemoteDiagnosticError(x: any): x is IRemoteDiagnosticError {
return !!x.hostName && !!x.errorMessage;
}

View File

@@ -0,0 +1,577 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as osLib from 'os';
import { virtualMachineHint } from 'vs/base/node/id';
import { IMachineInfo, WorkspaceStats, WorkspaceStatItem, PerformanceInfo, SystemInfo, IRemoteDiagnosticInfo, IRemoteDiagnosticError, isRemoteDiagnosticError, IWorkspaceInformation } from 'vs/platform/diagnostics/common/diagnostics';
import { readdir, exists, readFile } from 'fs';
import { join, basename } from 'vs/base/common/path';
import { parse, ParseError, getNodeType } from 'vs/base/common/json';
import { listProcesses } from 'vs/base/node/ps';
import product from 'vs/platform/product/common/product';
import { isWindows, isLinux } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { ProcessItem } from 'vs/base/common/processes';
import { IMainProcessInfo } from 'vs/platform/launch/node/launch';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Iterable } from 'vs/base/common/iterator';
import { Schemas } from 'vs/base/common/network';
export const ID = 'diagnosticsService';
export const IDiagnosticsService = createDecorator<IDiagnosticsService>(ID);
export interface IDiagnosticsService {
readonly _serviceBrand: undefined;
getPerformanceInfo(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<PerformanceInfo>;
getSystemInfo(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<SystemInfo>;
getDiagnostics(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<string>;
reportWorkspaceStats(workspace: IWorkspaceInformation): Promise<void>;
}
export interface VersionInfo {
vscodeVersion: string;
os: string;
}
export interface ProcessInfo {
cpu: number;
memory: number;
pid: number;
name: string;
}
interface ConfigFilePatterns {
tag: string;
filePattern: RegExp;
relativePathPattern?: RegExp;
}
export async function collectWorkspaceStats(folder: string, filter: string[]): Promise<WorkspaceStats> {
const configFilePatterns: ConfigFilePatterns[] = [
{ tag: 'grunt.js', filePattern: /^gruntfile\.js$/i },
{ tag: 'gulp.js', filePattern: /^gulpfile\.js$/i },
{ tag: 'tsconfig.json', filePattern: /^tsconfig\.json$/i },
{ tag: 'package.json', filePattern: /^package\.json$/i },
{ tag: 'jsconfig.json', filePattern: /^jsconfig\.json$/i },
{ tag: 'tslint.json', filePattern: /^tslint\.json$/i },
{ tag: 'eslint.json', filePattern: /^eslint\.json$/i },
{ tag: 'tasks.json', filePattern: /^tasks\.json$/i },
{ tag: 'launch.json', filePattern: /^launch\.json$/i },
{ tag: 'settings.json', filePattern: /^settings\.json$/i },
{ tag: 'webpack.config.js', filePattern: /^webpack\.config\.js$/i },
{ tag: 'project.json', filePattern: /^project\.json$/i },
{ tag: 'makefile', filePattern: /^makefile$/i },
{ tag: 'sln', filePattern: /^.+\.sln$/i },
{ tag: 'csproj', filePattern: /^.+\.csproj$/i },
{ tag: 'cmake', filePattern: /^.+\.cmake$/i },
{ tag: 'github-actions', filePattern: /^.+\.yml$/i, relativePathPattern: /^\.github(?:\/|\\)workflows$/i }
];
const fileTypes = new Map<string, number>();
const configFiles = new Map<string, number>();
const MAX_FILES = 20000;
function collect(root: string, dir: string, filter: string[], token: { count: number, maxReached: boolean }): Promise<void> {
const relativePath = dir.substring(root.length + 1);
return new Promise(resolve => {
readdir(dir, { withFileTypes: true }, async (err, files) => {
// Ignore folders that can't be read
if (err) {
resolve();
return;
}
if (token.count >= MAX_FILES) {
token.count += files.length;
token.maxReached = true;
resolve();
return;
}
let pending = files.length;
if (pending === 0) {
resolve();
return;
}
let filesToRead = files;
if (token.count + files.length > MAX_FILES) {
token.maxReached = true;
pending = MAX_FILES - token.count;
filesToRead = files.slice(0, pending);
}
token.count += files.length;
for (const file of filesToRead) {
if (file.isDirectory()) {
if (!filter.includes(file.name)) {
await collect(root, join(dir, file.name), filter, token);
}
if (--pending === 0) {
resolve();
return;
}
} else {
const index = file.name.lastIndexOf('.');
if (index >= 0) {
const fileType = file.name.substring(index + 1);
if (fileType) {
fileTypes.set(fileType, (fileTypes.get(fileType) ?? 0) + 1);
}
}
for (const configFile of configFilePatterns) {
if (configFile.relativePathPattern?.test(relativePath) !== false && configFile.filePattern.test(file.name)) {
configFiles.set(configFile.tag, (configFiles.get(configFile.tag) ?? 0) + 1);
}
}
if (--pending === 0) {
resolve();
return;
}
}
}
});
});
}
const token: { count: number, maxReached: boolean } = { count: 0, maxReached: false };
await collect(folder, folder, filter, token);
const launchConfigs = await collectLaunchConfigs(folder);
return {
configFiles: asSortedItems(configFiles),
fileTypes: asSortedItems(fileTypes),
fileCount: token.count,
maxFilesReached: token.maxReached,
launchConfigFiles: launchConfigs
};
}
function asSortedItems(items: Map<string, number>): WorkspaceStatItem[] {
return [
...Iterable.map(items.entries(), ([name, count]) => ({ name: name, count: count }))
].sort((a, b) => b.count - a.count);
}
export function getMachineInfo(): IMachineInfo {
const MB = 1024 * 1024;
const GB = 1024 * MB;
const machineInfo: IMachineInfo = {
os: `${osLib.type()} ${osLib.arch()} ${osLib.release()}`,
memory: `${(osLib.totalmem() / GB).toFixed(2)}GB (${(osLib.freemem() / GB).toFixed(2)}GB free)`,
vmHint: `${Math.round((virtualMachineHint.value() * 100))}%`,
};
const cpus = osLib.cpus();
if (cpus && cpus.length > 0) {
machineInfo.cpus = `${cpus[0].model} (${cpus.length} x ${cpus[0].speed})`;
}
return machineInfo;
}
export function collectLaunchConfigs(folder: string): Promise<WorkspaceStatItem[]> {
let launchConfigs = new Map<string, number>();
let launchConfig = join(folder, '.vscode', 'launch.json');
return new Promise((resolve, reject) => {
exists(launchConfig, (doesExist) => {
if (doesExist) {
readFile(launchConfig, (err, contents) => {
if (err) {
return resolve([]);
}
const errors: ParseError[] = [];
const json = parse(contents.toString(), errors);
if (errors.length) {
console.log(`Unable to parse ${launchConfig}`);
return resolve([]);
}
if (getNodeType(json) === 'object' && json['configurations']) {
for (const each of json['configurations']) {
const type = each['type'];
if (type) {
if (launchConfigs.has(type)) {
launchConfigs.set(type, launchConfigs.get(type)! + 1);
} else {
launchConfigs.set(type, 1);
}
}
}
}
return resolve(asSortedItems(launchConfigs));
});
} else {
return resolve([]);
}
});
});
}
export class DiagnosticsService implements IDiagnosticsService {
declare readonly _serviceBrand: undefined;
constructor(@ITelemetryService private readonly telemetryService: ITelemetryService) { }
private formatMachineInfo(info: IMachineInfo): string {
const output: string[] = [];
output.push(`OS Version: ${info.os}`);
output.push(`CPUs: ${info.cpus}`);
output.push(`Memory (System): ${info.memory}`);
output.push(`VM: ${info.vmHint}`);
return output.join('\n');
}
private formatEnvironment(info: IMainProcessInfo): string {
const MB = 1024 * 1024;
const GB = 1024 * MB;
const output: string[] = [];
output.push(`Version: ${product.nameShort} ${product.version} (${product.commit || 'Commit unknown'}, ${product.date || 'Date unknown'})`);
output.push(`OS Version: ${osLib.type()} ${osLib.arch()} ${osLib.release()}`);
const cpus = osLib.cpus();
if (cpus && cpus.length > 0) {
output.push(`CPUs: ${cpus[0].model} (${cpus.length} x ${cpus[0].speed})`);
}
output.push(`Memory (System): ${(osLib.totalmem() / GB).toFixed(2)}GB (${(osLib.freemem() / GB).toFixed(2)}GB free)`);
if (!isWindows) {
output.push(`Load (avg): ${osLib.loadavg().map(l => Math.round(l)).join(', ')}`); // only provided on Linux/macOS
}
output.push(`VM: ${Math.round((virtualMachineHint.value() * 100))}%`);
output.push(`Screen Reader: ${info.screenReader ? 'yes' : 'no'}`);
output.push(`Process Argv: ${info.mainArguments.join(' ')}`);
output.push(`GPU Status: ${this.expandGPUFeatures(info.gpuFeatureStatus)}`);
return output.join('\n');
}
public async getPerformanceInfo(info: IMainProcessInfo, remoteData: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<PerformanceInfo> {
return Promise.all<ProcessItem, string>([listProcesses(info.mainPID), this.formatWorkspaceMetadata(info)]).then(async result => {
let [rootProcess, workspaceInfo] = result;
let processInfo = this.formatProcessList(info, rootProcess);
remoteData.forEach(diagnostics => {
if (isRemoteDiagnosticError(diagnostics)) {
processInfo += `\n${diagnostics.errorMessage}`;
workspaceInfo += `\n${diagnostics.errorMessage}`;
} else {
processInfo += `\n\nRemote: ${diagnostics.hostName}`;
if (diagnostics.processes) {
processInfo += `\n${this.formatProcessList(info, diagnostics.processes)}`;
}
if (diagnostics.workspaceMetadata) {
workspaceInfo += `\n| Remote: ${diagnostics.hostName}`;
for (const folder of Object.keys(diagnostics.workspaceMetadata)) {
const metadata = diagnostics.workspaceMetadata[folder];
let countMessage = `${metadata.fileCount} files`;
if (metadata.maxFilesReached) {
countMessage = `more than ${countMessage}`;
}
workspaceInfo += `| Folder (${folder}): ${countMessage}`;
workspaceInfo += this.formatWorkspaceStats(metadata);
}
}
}
});
return {
processInfo,
workspaceInfo
};
});
}
public async getSystemInfo(info: IMainProcessInfo, remoteData: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<SystemInfo> {
const { memory, vmHint, os, cpus } = getMachineInfo();
const systemInfo: SystemInfo = {
os,
memory,
cpus,
vmHint,
processArgs: `${info.mainArguments.join(' ')}`,
gpuStatus: info.gpuFeatureStatus,
screenReader: `${info.screenReader ? 'yes' : 'no'}`,
remoteData
};
if (!isWindows) {
systemInfo.load = `${osLib.loadavg().map(l => Math.round(l)).join(', ')}`;
}
if (isLinux) {
systemInfo.linuxEnv = {
desktopSession: process.env.DESKTOP_SESSION,
xdgSessionDesktop: process.env.XDG_SESSION_DESKTOP,
xdgCurrentDesktop: process.env.XDG_CURRENT_DESKTOP,
xdgSessionType: process.env.XDG_SESSION_TYPE
};
}
return Promise.resolve(systemInfo);
}
public async getDiagnostics(info: IMainProcessInfo, remoteDiagnostics: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<string> {
const output: string[] = [];
return listProcesses(info.mainPID).then(async rootProcess => {
// Environment Info
output.push('');
output.push(this.formatEnvironment(info));
// Process List
output.push('');
output.push(this.formatProcessList(info, rootProcess));
// Workspace Stats
if (info.windows.some(window => window.folderURIs && window.folderURIs.length > 0 && !window.remoteAuthority)) {
output.push('');
output.push('Workspace Stats: ');
output.push(await this.formatWorkspaceMetadata(info));
}
remoteDiagnostics.forEach(diagnostics => {
if (isRemoteDiagnosticError(diagnostics)) {
output.push(`\n${diagnostics.errorMessage}`);
} else {
output.push('\n\n');
output.push(`Remote: ${diagnostics.hostName}`);
output.push(this.formatMachineInfo(diagnostics.machineInfo));
if (diagnostics.processes) {
output.push(this.formatProcessList(info, diagnostics.processes));
}
if (diagnostics.workspaceMetadata) {
for (const folder of Object.keys(diagnostics.workspaceMetadata)) {
const metadata = diagnostics.workspaceMetadata[folder];
let countMessage = `${metadata.fileCount} files`;
if (metadata.maxFilesReached) {
countMessage = `more than ${countMessage}`;
}
output.push(`Folder (${folder}): ${countMessage}`);
output.push(this.formatWorkspaceStats(metadata));
}
}
}
});
output.push('');
output.push('');
return output.join('\n');
});
}
private formatWorkspaceStats(workspaceStats: WorkspaceStats): string {
const output: string[] = [];
const lineLength = 60;
let col = 0;
const appendAndWrap = (name: string, count: number) => {
const item = ` ${name}(${count})`;
if (col + item.length > lineLength) {
output.push(line);
line = '| ';
col = line.length;
}
else {
col += item.length;
}
line += item;
};
// File Types
let line = '| File types:';
const maxShown = 10;
let max = workspaceStats.fileTypes.length > maxShown ? maxShown : workspaceStats.fileTypes.length;
for (let i = 0; i < max; i++) {
const item = workspaceStats.fileTypes[i];
appendAndWrap(item.name, item.count);
}
output.push(line);
// Conf Files
if (workspaceStats.configFiles.length >= 0) {
line = '| Conf files:';
col = 0;
workspaceStats.configFiles.forEach((item) => {
appendAndWrap(item.name, item.count);
});
output.push(line);
}
if (workspaceStats.launchConfigFiles.length > 0) {
let line = '| Launch Configs:';
workspaceStats.launchConfigFiles.forEach(each => {
const item = each.count > 1 ? ` ${each.name}(${each.count})` : ` ${each.name}`;
line += item;
});
output.push(line);
}
return output.join('\n');
}
private expandGPUFeatures(gpuFeatures: any): string {
const longestFeatureName = Math.max(...Object.keys(gpuFeatures).map(feature => feature.length));
// Make columns aligned by adding spaces after feature name
return Object.keys(gpuFeatures).map(feature => `${feature}: ${' '.repeat(longestFeatureName - feature.length)} ${gpuFeatures[feature]}`).join('\n ');
}
private formatWorkspaceMetadata(info: IMainProcessInfo): Promise<string> {
const output: string[] = [];
const workspaceStatPromises: Promise<void>[] = [];
info.windows.forEach(window => {
if (window.folderURIs.length === 0 || !!window.remoteAuthority) {
return;
}
output.push(`| Window (${window.title})`);
window.folderURIs.forEach(uriComponents => {
const folderUri = URI.revive(uriComponents);
if (folderUri.scheme === Schemas.file) {
const folder = folderUri.fsPath;
workspaceStatPromises.push(collectWorkspaceStats(folder, ['node_modules', '.git']).then(stats => {
let countMessage = `${stats.fileCount} files`;
if (stats.maxFilesReached) {
countMessage = `more than ${countMessage}`;
}
output.push(`| Folder (${basename(folder)}): ${countMessage}`);
output.push(this.formatWorkspaceStats(stats));
}).catch(error => {
output.push(`| Error: Unable to collect workspace stats for folder ${folder} (${error.toString()})`);
}));
} else {
output.push(`| Folder (${folderUri.toString()}): Workspace stats not available.`);
}
});
});
return Promise.all(workspaceStatPromises)
.then(_ => output.join('\n'))
.catch(e => `Unable to collect workspace stats: ${e}`);
}
private formatProcessList(info: IMainProcessInfo, rootProcess: ProcessItem): string {
const mapPidToWindowTitle = new Map<number, string>();
info.windows.forEach(window => mapPidToWindowTitle.set(window.pid, window.title));
const output: string[] = [];
output.push('CPU %\tMem MB\t PID\tProcess');
if (rootProcess) {
this.formatProcessItem(info.mainPID, mapPidToWindowTitle, output, rootProcess, 0);
}
return output.join('\n');
}
private formatProcessItem(mainPid: number, mapPidToWindowTitle: Map<number, string>, output: string[], item: ProcessItem, indent: number): void {
const isRoot = (indent === 0);
const MB = 1024 * 1024;
// Format name with indent
let name: string;
if (isRoot) {
name = item.pid === mainPid ? `${product.applicationName} main` : 'remote agent';
} else {
name = `${' '.repeat(indent)} ${item.name}`;
if (item.name === 'window') {
name = `${name} (${mapPidToWindowTitle.get(item.pid)})`;
}
}
const memory = process.platform === 'win32' ? item.mem : (osLib.totalmem() * (item.mem / 100));
output.push(`${item.load.toFixed(0).padStart(5, ' ')}\t${(memory / MB).toFixed(0).padStart(6, ' ')}\t${item.pid.toFixed(0).padStart(6, ' ')}\t${name}`);
// Recurse into children if any
if (Array.isArray(item.children)) {
item.children.forEach(child => this.formatProcessItem(mainPid, mapPidToWindowTitle, output, child, indent + 1));
}
}
public async reportWorkspaceStats(workspace: IWorkspaceInformation): Promise<void> {
for (const { uri } of workspace.folders) {
const folderUri = URI.revive(uri);
if (folderUri.scheme !== Schemas.file) {
continue;
}
const folder = folderUri.fsPath;
try {
const stats = await collectWorkspaceStats(folder, ['node_modules', '.git']);
type WorkspaceStatsClassification = {
'workspace.id': { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
rendererSessionId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
};
type WorkspaceStatsEvent = {
'workspace.id': string | undefined;
rendererSessionId: string;
};
this.telemetryService.publicLog2<WorkspaceStatsEvent, WorkspaceStatsClassification>('workspace.stats', {
'workspace.id': workspace.telemetryId,
rendererSessionId: workspace.rendererSessionId
});
type WorkspaceStatsFileClassification = {
rendererSessionId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
type: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
count: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
type WorkspaceStatsFileEvent = {
rendererSessionId: string;
type: string;
count: number;
};
stats.fileTypes.forEach(e => {
this.telemetryService.publicLog2<WorkspaceStatsFileEvent, WorkspaceStatsFileClassification>('workspace.stats.file', {
rendererSessionId: workspace.rendererSessionId,
type: e.name,
count: e.count
});
});
stats.launchConfigFiles.forEach(e => {
this.telemetryService.publicLog2<WorkspaceStatsFileEvent, WorkspaceStatsFileClassification>('workspace.stats.launchConfigFile', {
rendererSessionId: workspace.rendererSessionId,
type: e.name,
count: e.count
});
});
stats.configFiles.forEach(e => {
this.telemetryService.publicLog2<WorkspaceStatsFileEvent, WorkspaceStatsFileClassification>('workspace.stats.configFiles', {
rendererSessionId: workspace.rendererSessionId,
type: e.name,
count: e.count
});
});
} catch {
// Report nothing if collecting metadata fails.
}
}
}
}

View File

@@ -0,0 +1,307 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import Severity from 'vs/base/common/severity';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { URI } from 'vs/base/common/uri';
import { basename } from 'vs/base/common/resources';
import { localize } from 'vs/nls';
import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
export interface FileFilter {
extensions: string[];
name: string;
}
export type DialogType = 'none' | 'info' | 'error' | 'question' | 'warning';
export interface ICheckbox {
label: string;
checked?: boolean;
}
export interface IConfirmation {
title?: string;
type?: DialogType;
message: string;
detail?: string;
primaryButton?: string;
secondaryButton?: string;
checkbox?: ICheckbox;
}
export interface IConfirmationResult {
/**
* Will be true if the dialog was confirmed with the primary button
* pressed.
*/
confirmed: boolean;
/**
* This will only be defined if the confirmation was created
* with the checkbox option defined.
*/
checkboxChecked?: boolean;
}
export interface IShowResult {
/**
* Selected choice index. If the user refused to choose,
* then a promise with index of `cancelId` option is returned. If there is no such
* option then promise with index `0` is returned.
*/
choice: number;
/**
* This will only be defined if the confirmation was created
* with the checkbox option defined.
*/
checkboxChecked?: boolean;
}
export interface IInputResult extends IShowResult {
/**
* Values for the input fields as provided by the user
* or `undefined` if none.
*/
values?: string[];
}
export interface IPickAndOpenOptions {
forceNewWindow?: boolean;
defaultUri?: URI;
telemetryExtraData?: ITelemetryData;
availableFileSystems?: string[];
}
export interface ISaveDialogOptions {
/**
* A human-readable string for the dialog title
*/
title?: string;
/**
* The resource the dialog shows when opened.
*/
defaultUri?: URI;
/**
* A set of file filters that are used by the dialog. Each entry is a human readable label,
* like "TypeScript", and an array of extensions.
*/
filters?: FileFilter[];
/**
* A human-readable string for the ok button
*/
saveLabel?: string;
/**
* Specifies a list of schemas for the file systems the user can save to. If not specified, uses the schema of the defaultURI or, if also not specified,
* the schema of the current window.
*/
availableFileSystems?: readonly string[];
}
export interface IOpenDialogOptions {
/**
* A human-readable string for the dialog title
*/
title?: string;
/**
* The resource the dialog shows when opened.
*/
defaultUri?: URI;
/**
* A human-readable string for the open button.
*/
openLabel?: string;
/**
* Allow to select files, defaults to `true`.
*/
canSelectFiles?: boolean;
/**
* Allow to select folders, defaults to `false`.
*/
canSelectFolders?: boolean;
/**
* Allow to select many files or folders.
*/
canSelectMany?: boolean;
/**
* A set of file filters that are used by the dialog. Each entry is a human readable label,
* like "TypeScript", and an array of extensions.
*/
filters?: FileFilter[];
/**
* Specifies a list of schemas for the file systems the user can load from. If not specified, uses the schema of the defaultURI or, if also not available,
* the schema of the current window.
*/
availableFileSystems?: readonly string[];
}
export const IDialogService = createDecorator<IDialogService>('dialogService');
export interface IDialogOptions {
cancelId?: number;
detail?: string;
checkbox?: ICheckbox;
}
export interface IInput {
placeholder?: string;
type?: 'text' | 'password'
value?: string;
}
/**
* A service to bring up modal dialogs.
*
* Note: use the `INotificationService.prompt()` method for a non-modal way to ask
* the user for input.
*/
export interface IDialogService {
readonly _serviceBrand: undefined;
/**
* Ask the user for confirmation with a modal dialog.
*/
confirm(confirmation: IConfirmation): Promise<IConfirmationResult>;
/**
* Present a modal dialog to the user.
*
* @returns A promise with the selected choice index. If the user refused to choose,
* then a promise with index of `cancelId` option is returned. If there is no such
* option then promise with index `0` is returned.
*/
show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise<IShowResult>;
/**
* Present a modal dialog to the user asking for input.
*
* @returns A promise with the selected choice index. If the user refused to choose,
* then a promise with index of `cancelId` option is returned. If there is no such
* option then promise with index `0` is returned. In addition, the values for the
* inputs are returned as well.
*/
input(severity: Severity, message: string, buttons: string[], inputs: IInput[], options?: IDialogOptions): Promise<IInputResult>;
/**
* Present the about dialog to the user.
*/
about(): Promise<void>;
}
export const IFileDialogService = createDecorator<IFileDialogService>('fileDialogService');
/**
* A service to bring up file dialogs.
*/
export interface IFileDialogService {
readonly _serviceBrand: undefined;
/**
* The default path for a new file based on previously used files.
* @param schemeFilter The scheme of the file path. If no filter given, the scheme of the current window is used.
*/
defaultFilePath(schemeFilter?: string): URI | undefined;
/**
* The default path for a new folder based on previously used folders.
* @param schemeFilter The scheme of the folder path. If no filter given, the scheme of the current window is used.
*/
defaultFolderPath(schemeFilter?: string): URI | undefined;
/**
* The default path for a new workspace based on previously used workspaces.
* @param schemeFilter The scheme of the workspace path. If no filter given, the scheme of the current window is used.
*/
defaultWorkspacePath(schemeFilter?: string, filename?: string): URI | undefined;
/**
* Shows a file-folder selection dialog and opens the selected entry.
*/
pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise<void>;
/**
* Shows a file selection dialog and opens the selected entry.
*/
pickFileAndOpen(options: IPickAndOpenOptions): Promise<void>;
/**
* Shows a folder selection dialog and opens the selected entry.
*/
pickFolderAndOpen(options: IPickAndOpenOptions): Promise<void>;
/**
* Shows a workspace selection dialog and opens the selected entry.
*/
pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise<void>;
/**
* Shows a save file dialog and save the file at the chosen file URI.
*/
pickFileToSave(defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined>;
/**
* Shows a save file dialog and returns the chosen file URI.
*/
showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined>;
/**
* Shows a confirm dialog for saving 1-N files.
*/
showSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise<ConfirmResult>;
/**
* Shows a open file dialog and returns the chosen file URI.
*/
showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined>;
}
export const enum ConfirmResult {
SAVE,
DONT_SAVE,
CANCEL
}
const MAX_CONFIRM_FILES = 10;
export function getFileNamesMessage(fileNamesOrResources: readonly (string | URI)[]): string {
const message: string[] = [];
message.push(...fileNamesOrResources.slice(0, MAX_CONFIRM_FILES).map(fileNameOrResource => typeof fileNameOrResource === 'string' ? fileNameOrResource : basename(fileNameOrResource)));
if (fileNamesOrResources.length > MAX_CONFIRM_FILES) {
if (fileNamesOrResources.length - MAX_CONFIRM_FILES === 1) {
message.push(localize('moreFile', "...1 additional file not shown"));
} else {
message.push(localize('moreFiles', "...{0} additional files not shown", fileNamesOrResources.length - MAX_CONFIRM_FILES));
}
}
message.push('');
return message.join('\n');
}
export interface INativeOpenDialogOptions {
forceNewWindow?: boolean;
defaultPath?: string;
telemetryEventName?: string;
telemetryExtraData?: ITelemetryData;
}

View File

@@ -0,0 +1,207 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { MessageBoxOptions, MessageBoxReturnValue, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, dialog, FileFilter, BrowserWindow } from 'electron';
import { Queue } from 'vs/base/common/async';
import { IStateService } from 'vs/platform/state/node/state';
import { isMacintosh } from 'vs/base/common/platform';
import { dirname } from 'vs/base/common/path';
import { normalizeNFC } from 'vs/base/common/normalization';
import { exists } from 'vs/base/node/pfs';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { withNullAsUndefined } from 'vs/base/common/types';
import { localize } from 'vs/nls';
import { WORKSPACE_FILTER } from 'vs/platform/workspaces/common/workspaces';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
export const IDialogMainService = createDecorator<IDialogMainService>('dialogMainService');
export interface IDialogMainService {
readonly _serviceBrand: undefined;
pickFileFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise<string[] | undefined>;
pickFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise<string[] | undefined>;
pickFile(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise<string[] | undefined>;
pickWorkspace(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise<string[] | undefined>;
showMessageBox(options: MessageBoxOptions, window?: BrowserWindow): Promise<MessageBoxReturnValue>;
showSaveDialog(options: SaveDialogOptions, window?: BrowserWindow): Promise<SaveDialogReturnValue>;
showOpenDialog(options: OpenDialogOptions, window?: BrowserWindow): Promise<OpenDialogReturnValue>;
}
interface IInternalNativeOpenDialogOptions extends INativeOpenDialogOptions {
pickFolders?: boolean;
pickFiles?: boolean;
title: string;
buttonLabel?: string;
filters?: FileFilter[];
}
export class DialogMainService implements IDialogMainService {
declare readonly _serviceBrand: undefined;
private static readonly workingDirPickerStorageKey = 'pickerWorkingDir';
private readonly mapWindowToDialogQueue: Map<number, Queue<void>>;
private readonly noWindowDialogQueue: Queue<void>;
constructor(
@IStateService private readonly stateService: IStateService
) {
this.mapWindowToDialogQueue = new Map<number, Queue<void>>();
this.noWindowDialogQueue = new Queue<void>();
}
pickFileFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise<string[] | undefined> {
return this.doPick({ ...options, pickFolders: true, pickFiles: true, title: localize('open', "Open") }, window);
}
pickFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise<string[] | undefined> {
return this.doPick({ ...options, pickFolders: true, title: localize('openFolder', "Open Folder") }, window);
}
pickFile(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise<string[] | undefined> {
return this.doPick({ ...options, pickFiles: true, title: localize('openFile', "Open File") }, window);
}
pickWorkspace(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise<string[] | undefined> {
const title = localize('openWorkspaceTitle', "Open Workspace");
const buttonLabel = mnemonicButtonLabel(localize({ key: 'openWorkspace', comment: ['&& denotes a mnemonic'] }, "&&Open"));
const filters = WORKSPACE_FILTER;
return this.doPick({ ...options, pickFiles: true, title, filters, buttonLabel }, window);
}
private async doPick(options: IInternalNativeOpenDialogOptions, window?: BrowserWindow): Promise<string[] | undefined> {
// Ensure dialog options
const dialogOptions: OpenDialogOptions = {
title: options.title,
buttonLabel: options.buttonLabel,
filters: options.filters
};
// Ensure defaultPath
dialogOptions.defaultPath = options.defaultPath || this.stateService.getItem<string>(DialogMainService.workingDirPickerStorageKey);
// Ensure properties
if (typeof options.pickFiles === 'boolean' || typeof options.pickFolders === 'boolean') {
dialogOptions.properties = undefined; // let it override based on the booleans
if (options.pickFiles && options.pickFolders) {
dialogOptions.properties = ['multiSelections', 'openDirectory', 'openFile', 'createDirectory'];
}
}
if (!dialogOptions.properties) {
dialogOptions.properties = ['multiSelections', options.pickFolders ? 'openDirectory' : 'openFile', 'createDirectory'];
}
if (isMacintosh) {
dialogOptions.properties.push('treatPackageAsDirectory'); // always drill into .app files
}
// Show Dialog
const windowToUse = window || BrowserWindow.getFocusedWindow();
const result = await this.showOpenDialog(dialogOptions, withNullAsUndefined(windowToUse));
if (result && result.filePaths && result.filePaths.length > 0) {
// Remember path in storage for next time
this.stateService.setItem(DialogMainService.workingDirPickerStorageKey, dirname(result.filePaths[0]));
return result.filePaths;
}
return;
}
private getDialogQueue(window?: BrowserWindow): Queue<any> {
if (!window) {
return this.noWindowDialogQueue;
}
let windowDialogQueue = this.mapWindowToDialogQueue.get(window.id);
if (!windowDialogQueue) {
windowDialogQueue = new Queue<any>();
this.mapWindowToDialogQueue.set(window.id, windowDialogQueue);
}
return windowDialogQueue;
}
showMessageBox(options: MessageBoxOptions, window?: BrowserWindow): Promise<MessageBoxReturnValue> {
return this.getDialogQueue(window).queue(async () => {
if (window) {
return dialog.showMessageBox(window, options);
}
return dialog.showMessageBox(options);
});
}
showSaveDialog(options: SaveDialogOptions, window?: BrowserWindow): Promise<SaveDialogReturnValue> {
function normalizePath(path: string | undefined): string | undefined {
if (path && isMacintosh) {
path = normalizeNFC(path); // normalize paths returned from the OS
}
return path;
}
return this.getDialogQueue(window).queue(async () => {
let result: SaveDialogReturnValue;
if (window) {
result = await dialog.showSaveDialog(window, options);
} else {
result = await dialog.showSaveDialog(options);
}
result.filePath = normalizePath(result.filePath);
return result;
});
}
showOpenDialog(options: OpenDialogOptions, window?: BrowserWindow): Promise<OpenDialogReturnValue> {
function normalizePaths(paths: string[]): string[] {
if (paths && paths.length > 0 && isMacintosh) {
paths = paths.map(path => normalizeNFC(path)); // normalize paths returned from the OS
}
return paths;
}
return this.getDialogQueue(window).queue(async () => {
// Ensure the path exists (if provided)
if (options.defaultPath) {
const pathExists = await exists(options.defaultPath);
if (!pathExists) {
options.defaultPath = undefined;
}
}
// Show dialog
let result: OpenDialogReturnValue;
if (window) {
result = await dialog.showOpenDialog(window, options);
} else {
result = await dialog.showOpenDialog(options);
}
result.filePaths = normalizePaths(result.filePaths);
return result;
});
}
}

View File

@@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import Severity from 'vs/base/common/severity';
import { IConfirmation, IConfirmationResult, IDialogService, IDialogOptions, IShowResult, IInputResult } from 'vs/platform/dialogs/common/dialogs';
export class TestDialogService implements IDialogService {
declare readonly _serviceBrand: undefined;
confirm(_confirmation: IConfirmation): Promise<IConfirmationResult> { return Promise.resolve({ confirmed: false }); }
show(_severity: Severity, _message: string, _buttons: string[], _options?: IDialogOptions): Promise<IShowResult> { return Promise.resolve({ choice: 0 }); }
input(): Promise<IInputResult> { { return Promise.resolve({ choice: 0, values: [] }); } }
about(): Promise<void> { return Promise.resolve(); }
}

View File

@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { CancellationToken } from 'vs/base/common/cancellation';
export const IDownloadService = createDecorator<IDownloadService>('downloadService');
export interface IDownloadService {
readonly _serviceBrand: undefined;
download(uri: URI, to: URI, cancellationToken?: CancellationToken): Promise<void>;
}

View File

@@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event } from 'vs/base/common/event';
import { IDownloadService } from 'vs/platform/download/common/download';
import { IURITransformer } from 'vs/base/common/uriIpc';
export class DownloadServiceChannel implements IServerChannel {
constructor(private readonly service: IDownloadService) { }
listen(_: unknown, event: string, arg?: any): Event<any> {
throw new Error('Invalid listen');
}
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'download': return this.service.download(URI.revive(args[0]), URI.revive(args[1]));
}
throw new Error('Invalid call');
}
}
export class DownloadServiceChannelClient implements IDownloadService {
declare readonly _serviceBrand: undefined;
constructor(private channel: IChannel, private getUriTransformer: () => IURITransformer | null) { }
async download(from: URI, to: URI): Promise<void> {
const uriTransfomer = this.getUriTransformer();
if (uriTransfomer) {
from = uriTransfomer.transformOutgoingURI(from);
to = uriTransfomer.transformOutgoingURI(to);
}
await this.channel.call('download', [from, to]);
}
}

View File

@@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDownloadService } from 'vs/platform/download/common/download';
import { URI } from 'vs/base/common/uri';
import { IRequestService, asText } from 'vs/platform/request/common/request';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IFileService } from 'vs/platform/files/common/files';
import { Schemas } from 'vs/base/common/network';
export class DownloadService implements IDownloadService {
declare readonly _serviceBrand: undefined;
constructor(
@IRequestService private readonly requestService: IRequestService,
@IFileService private readonly fileService: IFileService
) { }
async download(resource: URI, target: URI, cancellationToken: CancellationToken = CancellationToken.None): Promise<void> {
if (resource.scheme === Schemas.file || resource.scheme === Schemas.vscodeRemote) {
// Intentionally only support this for file|remote<->file|remote scenarios
await this.fileService.copy(resource, target);
return;
}
const options = { type: 'GET', url: resource.toString() };
const context = await this.requestService.request(options, cancellationToken);
if (context.res.statusCode === 200) {
await this.fileService.writeFile(target, context.stream);
} else {
const message = await asText(context);
throw new Error(`Expected 200, got back ${context.res.statusCode} instead.\n\n${message}`);
}
}
}

View File

@@ -0,0 +1,190 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getTopLeftOffset, getClientArea } from 'vs/base/browser/dom';
import { coalesce } from 'vs/base/common/arrays';
import { IElement, IWindowDriver } from 'vs/platform/driver/common/driver';
function serializeElement(element: Element, recursive: boolean): IElement {
const attributes = Object.create(null);
for (let j = 0; j < element.attributes.length; j++) {
const attr = element.attributes.item(j);
if (attr) {
attributes[attr.name] = attr.value;
}
}
const children: IElement[] = [];
if (recursive) {
for (let i = 0; i < element.children.length; i++) {
const child = element.children.item(i);
if (child) {
children.push(serializeElement(child, true));
}
}
}
const { left, top } = getTopLeftOffset(element as HTMLElement);
return {
tagName: element.tagName,
className: element.className,
textContent: element.textContent || '',
attributes,
children,
left,
top
};
}
export abstract class BaseWindowDriver implements IWindowDriver {
abstract click(selector: string, xoffset?: number, yoffset?: number): Promise<void>;
abstract doubleClick(selector: string): Promise<void>;
async setValue(selector: string, text: string): Promise<void> {
const element = document.querySelector(selector);
if (!element) {
return Promise.reject(new Error(`Element not found: ${selector}`));
}
const inputElement = element as HTMLInputElement;
inputElement.value = text;
const event = new Event('input', { bubbles: true, cancelable: true });
inputElement.dispatchEvent(event);
}
async getTitle(): Promise<string> {
return document.title;
}
async isActiveElement(selector: string): Promise<boolean> {
const element = document.querySelector(selector);
if (element !== document.activeElement) {
const chain: string[] = [];
let el = document.activeElement;
while (el) {
const tagName = el.tagName;
const id = el.id ? `#${el.id}` : '';
const classes = coalesce(el.className.split(/\s+/g).map(c => c.trim())).map(c => `.${c}`).join('');
chain.unshift(`${tagName}${id}${classes}`);
el = el.parentElement;
}
throw new Error(`Active element not found. Current active element is '${chain.join(' > ')}'. Looking for ${selector}`);
}
return true;
}
async getElements(selector: string, recursive: boolean): Promise<IElement[]> {
const query = document.querySelectorAll(selector);
const result: IElement[] = [];
for (let i = 0; i < query.length; i++) {
const element = query.item(i);
result.push(serializeElement(element, recursive));
}
return result;
}
async getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number; }> {
const offset = typeof xoffset === 'number' && typeof yoffset === 'number' ? { x: xoffset, y: yoffset } : undefined;
return this._getElementXY(selector, offset);
}
async typeInEditor(selector: string, text: string): Promise<void> {
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Editor not found: ${selector}`);
}
const textarea = element as HTMLTextAreaElement;
const start = textarea.selectionStart;
const newStart = start + text.length;
const value = textarea.value;
const newValue = value.substr(0, start) + text + value.substr(start);
textarea.value = newValue;
textarea.setSelectionRange(newStart, newStart);
const event = new Event('input', { 'bubbles': true, 'cancelable': true });
textarea.dispatchEvent(event);
}
async getTerminalBuffer(selector: string): Promise<string[]> {
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Terminal not found: ${selector}`);
}
const xterm = (element as any).xterm;
if (!xterm) {
throw new Error(`Xterm not found: ${selector}`);
}
const lines: string[] = [];
for (let i = 0; i < xterm.buffer.length; i++) {
lines.push(xterm.buffer.getLine(i)!.translateToString(true));
}
return lines;
}
async writeInTerminal(selector: string, text: string): Promise<void> {
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Element not found: ${selector}`);
}
const xterm = (element as any).xterm;
if (!xterm) {
throw new Error(`Xterm not found: ${selector}`);
}
xterm._core._coreService.triggerDataEvent(text);
}
protected async _getElementXY(selector: string, offset?: { x: number, y: number }): Promise<{ x: number; y: number; }> {
const element = document.querySelector(selector);
if (!element) {
return Promise.reject(new Error(`Element not found: ${selector}`));
}
const { left, top } = getTopLeftOffset(element as HTMLElement);
const { width, height } = getClientArea(element as HTMLElement);
let x: number, y: number;
if (offset) {
x = left + offset.x;
y = top + offset.y;
} else {
x = left + (width / 2);
y = top + (height / 2);
}
x = Math.round(x);
y = Math.round(y);
return { x, y };
}
abstract openDevTools(): Promise<void>;
}

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { BaseWindowDriver } from 'vs/platform/driver/browser/baseDriver';
class BrowserWindowDriver extends BaseWindowDriver {
click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise<void> {
throw new Error('Method not implemented.');
}
doubleClick(selector: string): Promise<void> {
throw new Error('Method not implemented.');
}
openDevTools(): Promise<void> {
throw new Error('Method not implemented.');
}
}
export async function registerWindowDriver(): Promise<IDisposable> {
(<any>window).driver = new BrowserWindowDriver();
return Disposable.None;
}

View File

@@ -0,0 +1,56 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
// !! Do not remove the following START and END markers, they are parsed by the smoketest build
//*START
export interface IElement {
tagName: string;
className: string;
textContent: string;
attributes: { [name: string]: string; };
children: IElement[];
top: number;
left: number;
}
export interface IDriver {
readonly _serviceBrand: undefined;
getWindowIds(): Promise<number[]>;
capturePage(windowId: number): Promise<string>;
reloadWindow(windowId: number): Promise<void>;
exitApplication(): Promise<void>;
dispatchKeybinding(windowId: number, keybinding: string): Promise<void>;
click(windowId: number, selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise<void>;
doubleClick(windowId: number, selector: string): Promise<void>;
setValue(windowId: number, selector: string, text: string): Promise<void>;
getTitle(windowId: number): Promise<string>;
isActiveElement(windowId: number, selector: string): Promise<boolean>;
getElements(windowId: number, selector: string, recursive?: boolean): Promise<IElement[]>;
getElementXY(windowId: number, selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number; }>;
typeInEditor(windowId: number, selector: string, text: string): Promise<void>;
getTerminalBuffer(windowId: number, selector: string): Promise<string[]>;
writeInTerminal(windowId: number, selector: string, text: string): Promise<void>;
}
//*END
export const ID = 'driverService';
export const IDriver = createDecorator<IDriver>(ID);
export interface IWindowDriver {
click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise<void>;
doubleClick(selector: string): Promise<void>;
setValue(selector: string, text: string): Promise<void>;
getTitle(): Promise<string>;
isActiveElement(selector: string): Promise<boolean>;
getElements(selector: string, recursive: boolean): Promise<IElement[]>;
getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number; }>;
typeInEditor(selector: string, text: string): Promise<void>;
getTerminalBuffer(selector: string): Promise<string[]>;
writeInTerminal(selector: string, text: string): Promise<void>;
}

View File

@@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { WindowDriverChannel, WindowDriverRegistryChannelClient } from 'vs/platform/driver/node/driver';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { timeout } from 'vs/base/common/async';
import { BaseWindowDriver } from 'vs/platform/driver/browser/baseDriver';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
class WindowDriver extends BaseWindowDriver {
constructor(
@INativeHostService private readonly nativeHostService: INativeHostService
) {
super();
}
click(selector: string, xoffset?: number, yoffset?: number): Promise<void> {
const offset = typeof xoffset === 'number' && typeof yoffset === 'number' ? { x: xoffset, y: yoffset } : undefined;
return this._click(selector, 1, offset);
}
doubleClick(selector: string): Promise<void> {
return this._click(selector, 2);
}
private async _click(selector: string, clickCount: number, offset?: { x: number, y: number }): Promise<void> {
const { x, y } = await this._getElementXY(selector, offset);
await this.nativeHostService.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount } as any);
await timeout(10);
await this.nativeHostService.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount } as any);
await timeout(100);
}
async openDevTools(): Promise<void> {
await this.nativeHostService.openDevTools({ mode: 'detach' });
}
}
export async function registerWindowDriver(accessor: ServicesAccessor, windowId: number): Promise<IDisposable> {
const instantiationService = accessor.get(IInstantiationService);
const mainProcessService = accessor.get(IMainProcessService);
const windowDriver = instantiationService.createInstance(WindowDriver);
const windowDriverChannel = new WindowDriverChannel(windowDriver);
mainProcessService.registerChannel('windowDriver', windowDriverChannel);
const windowDriverRegistryChannel = mainProcessService.getChannel('windowDriverRegistry');
const windowDriverRegistry = new WindowDriverRegistryChannelClient(windowDriverRegistryChannel);
await windowDriverRegistry.registerWindowDriver(windowId);
// const options = await windowDriverRegistry.registerWindowDriver(windowId);
// if (options.verbose) {
// windowDriver.openDevTools();
// }
return toDisposable(() => windowDriverRegistry.reloadWindowDriver(windowId));
}

View File

@@ -0,0 +1,224 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DriverChannel, WindowDriverChannelClient, IWindowDriverRegistry, WindowDriverRegistryChannel, IDriverOptions } from 'vs/platform/driver/node/driver';
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
import { serve as serveNet } from 'vs/base/parts/ipc/node/ipc.net';
import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IPCServer, StaticRouter } from 'vs/base/parts/ipc/common/ipc';
import { SimpleKeybinding, KeyCode } from 'vs/base/common/keyCodes';
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
import { OS } from 'vs/base/common/platform';
import { Emitter, Event } from 'vs/base/common/event';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
import { ScanCodeBinding } from 'vs/base/common/scanCode';
import { KeybindingParser } from 'vs/base/common/keybindingParser';
import { timeout } from 'vs/base/common/async';
import { IDriver, IElement, IWindowDriver } from 'vs/platform/driver/common/driver';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService';
function isSilentKeyCode(keyCode: KeyCode) {
return keyCode < KeyCode.KEY_0;
}
export class Driver implements IDriver, IWindowDriverRegistry {
declare readonly _serviceBrand: undefined;
private registeredWindowIds = new Set<number>();
private reloadingWindowIds = new Set<number>();
private readonly onDidReloadingChange = new Emitter<void>();
constructor(
private windowServer: IPCServer,
private options: IDriverOptions,
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService
) { }
async registerWindowDriver(windowId: number): Promise<IDriverOptions> {
this.registeredWindowIds.add(windowId);
this.reloadingWindowIds.delete(windowId);
this.onDidReloadingChange.fire();
return this.options;
}
async reloadWindowDriver(windowId: number): Promise<void> {
this.reloadingWindowIds.add(windowId);
}
async getWindowIds(): Promise<number[]> {
return this.windowsMainService.getWindows()
.map(w => w.id)
.filter(id => this.registeredWindowIds.has(id) && !this.reloadingWindowIds.has(id));
}
async capturePage(windowId: number): Promise<string> {
await this.whenUnfrozen(windowId);
const window = this.windowsMainService.getWindowById(windowId);
if (!window) {
throw new Error('Invalid window');
}
const webContents = window.win.webContents;
const image = await webContents.capturePage();
return image.toPNG().toString('base64');
}
async reloadWindow(windowId: number): Promise<void> {
await this.whenUnfrozen(windowId);
const window = this.windowsMainService.getWindowById(windowId);
if (!window) {
throw new Error('Invalid window');
}
this.reloadingWindowIds.add(windowId);
this.lifecycleMainService.reload(window);
}
async exitApplication(): Promise<void> {
return this.nativeHostMainService.quit(undefined);
}
async dispatchKeybinding(windowId: number, keybinding: string): Promise<void> {
await this.whenUnfrozen(windowId);
const parts = KeybindingParser.parseUserBinding(keybinding);
for (let part of parts) {
await this._dispatchKeybinding(windowId, part);
}
}
private async _dispatchKeybinding(windowId: number, keybinding: SimpleKeybinding | ScanCodeBinding): Promise<void> {
if (keybinding instanceof ScanCodeBinding) {
throw new Error('ScanCodeBindings not supported');
}
const window = this.windowsMainService.getWindowById(windowId);
if (!window) {
throw new Error('Invalid window');
}
const webContents = window.win.webContents;
const noModifiedKeybinding = new SimpleKeybinding(false, false, false, false, keybinding.keyCode);
const resolvedKeybinding = new USLayoutResolvedKeybinding(noModifiedKeybinding.toChord(), OS);
const keyCode = resolvedKeybinding.getElectronAccelerator();
const modifiers: string[] = [];
if (keybinding.ctrlKey) {
modifiers.push('ctrl');
}
if (keybinding.metaKey) {
modifiers.push('meta');
}
if (keybinding.shiftKey) {
modifiers.push('shift');
}
if (keybinding.altKey) {
modifiers.push('alt');
}
webContents.sendInputEvent({ type: 'keyDown', keyCode, modifiers } as any);
if (!isSilentKeyCode(keybinding.keyCode)) {
webContents.sendInputEvent({ type: 'char', keyCode, modifiers } as any);
}
webContents.sendInputEvent({ type: 'keyUp', keyCode, modifiers } as any);
await timeout(100);
}
async click(windowId: number, selector: string, xoffset?: number, yoffset?: number): Promise<void> {
const windowDriver = await this.getWindowDriver(windowId);
await windowDriver.click(selector, xoffset, yoffset);
}
async doubleClick(windowId: number, selector: string): Promise<void> {
const windowDriver = await this.getWindowDriver(windowId);
await windowDriver.doubleClick(selector);
}
async setValue(windowId: number, selector: string, text: string): Promise<void> {
const windowDriver = await this.getWindowDriver(windowId);
await windowDriver.setValue(selector, text);
}
async getTitle(windowId: number): Promise<string> {
const windowDriver = await this.getWindowDriver(windowId);
return await windowDriver.getTitle();
}
async isActiveElement(windowId: number, selector: string): Promise<boolean> {
const windowDriver = await this.getWindowDriver(windowId);
return await windowDriver.isActiveElement(selector);
}
async getElements(windowId: number, selector: string, recursive: boolean): Promise<IElement[]> {
const windowDriver = await this.getWindowDriver(windowId);
return await windowDriver.getElements(selector, recursive);
}
async getElementXY(windowId: number, selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number; }> {
const windowDriver = await this.getWindowDriver(windowId);
return await windowDriver.getElementXY(selector, xoffset, yoffset);
}
async typeInEditor(windowId: number, selector: string, text: string): Promise<void> {
const windowDriver = await this.getWindowDriver(windowId);
await windowDriver.typeInEditor(selector, text);
}
async getTerminalBuffer(windowId: number, selector: string): Promise<string[]> {
const windowDriver = await this.getWindowDriver(windowId);
return await windowDriver.getTerminalBuffer(selector);
}
async writeInTerminal(windowId: number, selector: string, text: string): Promise<void> {
const windowDriver = await this.getWindowDriver(windowId);
await windowDriver.writeInTerminal(selector, text);
}
private async getWindowDriver(windowId: number): Promise<IWindowDriver> {
await this.whenUnfrozen(windowId);
const id = `window:${windowId}`;
const router = new StaticRouter(ctx => ctx === id);
const windowDriverChannel = this.windowServer.getChannel('windowDriver', router);
return new WindowDriverChannelClient(windowDriverChannel);
}
private async whenUnfrozen(windowId: number): Promise<void> {
while (this.reloadingWindowIds.has(windowId)) {
await Event.toPromise(this.onDidReloadingChange.event);
}
}
}
export async function serve(
windowServer: IPCServer,
handle: string,
environmentService: IEnvironmentMainService,
instantiationService: IInstantiationService
): Promise<IDisposable> {
const verbose = environmentService.driverVerbose;
const driver = instantiationService.createInstance(Driver, windowServer, { verbose });
const windowDriverRegistryChannel = new WindowDriverRegistryChannel(driver);
windowServer.registerChannel('windowDriverRegistry', windowDriverRegistryChannel);
const server = await serveNet(handle);
const channel = new DriverChannel(driver);
server.registerChannel('driver', channel);
return combinedDisposable(server, windowServer);
}

View File

@@ -0,0 +1,230 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Client } from 'vs/base/parts/ipc/common/ipc.net';
import { connect as connectNet } from 'vs/base/parts/ipc/node/ipc.net';
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event } from 'vs/base/common/event';
import { IDriver, IElement, IWindowDriver } from 'vs/platform/driver/common/driver';
export class DriverChannel implements IServerChannel {
constructor(private driver: IDriver) { }
listen<T>(_: unknown, event: string): Event<T> {
throw new Error('No event found');
}
call(_: unknown, command: string, arg?: any): Promise<any> {
switch (command) {
case 'getWindowIds': return this.driver.getWindowIds();
case 'capturePage': return this.driver.capturePage(arg);
case 'reloadWindow': return this.driver.reloadWindow(arg);
case 'exitApplication': return this.driver.exitApplication();
case 'dispatchKeybinding': return this.driver.dispatchKeybinding(arg[0], arg[1]);
case 'click': return this.driver.click(arg[0], arg[1], arg[2], arg[3]);
case 'doubleClick': return this.driver.doubleClick(arg[0], arg[1]);
case 'setValue': return this.driver.setValue(arg[0], arg[1], arg[2]);
case 'getTitle': return this.driver.getTitle(arg[0]);
case 'isActiveElement': return this.driver.isActiveElement(arg[0], arg[1]);
case 'getElements': return this.driver.getElements(arg[0], arg[1], arg[2]);
case 'getElementXY': return this.driver.getElementXY(arg[0], arg[1], arg[2]);
case 'typeInEditor': return this.driver.typeInEditor(arg[0], arg[1], arg[2]);
case 'getTerminalBuffer': return this.driver.getTerminalBuffer(arg[0], arg[1]);
case 'writeInTerminal': return this.driver.writeInTerminal(arg[0], arg[1], arg[2]);
}
throw new Error(`Call not found: ${command}`);
}
}
export class DriverChannelClient implements IDriver {
declare readonly _serviceBrand: undefined;
constructor(private channel: IChannel) { }
getWindowIds(): Promise<number[]> {
return this.channel.call('getWindowIds');
}
capturePage(windowId: number): Promise<string> {
return this.channel.call('capturePage', windowId);
}
reloadWindow(windowId: number): Promise<void> {
return this.channel.call('reloadWindow', windowId);
}
exitApplication(): Promise<void> {
return this.channel.call('exitApplication');
}
dispatchKeybinding(windowId: number, keybinding: string): Promise<void> {
return this.channel.call('dispatchKeybinding', [windowId, keybinding]);
}
click(windowId: number, selector: string, xoffset: number | undefined, yoffset: number | undefined): Promise<void> {
return this.channel.call('click', [windowId, selector, xoffset, yoffset]);
}
doubleClick(windowId: number, selector: string): Promise<void> {
return this.channel.call('doubleClick', [windowId, selector]);
}
setValue(windowId: number, selector: string, text: string): Promise<void> {
return this.channel.call('setValue', [windowId, selector, text]);
}
getTitle(windowId: number): Promise<string> {
return this.channel.call('getTitle', [windowId]);
}
isActiveElement(windowId: number, selector: string): Promise<boolean> {
return this.channel.call('isActiveElement', [windowId, selector]);
}
getElements(windowId: number, selector: string, recursive: boolean): Promise<IElement[]> {
return this.channel.call('getElements', [windowId, selector, recursive]);
}
getElementXY(windowId: number, selector: string, xoffset: number | undefined, yoffset: number | undefined): Promise<{ x: number, y: number }> {
return this.channel.call('getElementXY', [windowId, selector, xoffset, yoffset]);
}
typeInEditor(windowId: number, selector: string, text: string): Promise<void> {
return this.channel.call('typeInEditor', [windowId, selector, text]);
}
getTerminalBuffer(windowId: number, selector: string): Promise<string[]> {
return this.channel.call('getTerminalBuffer', [windowId, selector]);
}
writeInTerminal(windowId: number, selector: string, text: string): Promise<void> {
return this.channel.call('writeInTerminal', [windowId, selector, text]);
}
}
export interface IDriverOptions {
verbose: boolean;
}
export interface IWindowDriverRegistry {
registerWindowDriver(windowId: number): Promise<IDriverOptions>;
reloadWindowDriver(windowId: number): Promise<void>;
}
export class WindowDriverRegistryChannel implements IServerChannel {
constructor(private registry: IWindowDriverRegistry) { }
listen<T>(_: unknown, event: string): Event<T> {
throw new Error(`Event not found: ${event}`);
}
call(_: unknown, command: string, arg?: any): Promise<any> {
switch (command) {
case 'registerWindowDriver': return this.registry.registerWindowDriver(arg);
case 'reloadWindowDriver': return this.registry.reloadWindowDriver(arg);
}
throw new Error(`Call not found: ${command}`);
}
}
export class WindowDriverRegistryChannelClient implements IWindowDriverRegistry {
declare readonly _serviceBrand: undefined;
constructor(private channel: IChannel) { }
registerWindowDriver(windowId: number): Promise<IDriverOptions> {
return this.channel.call('registerWindowDriver', windowId);
}
reloadWindowDriver(windowId: number): Promise<void> {
return this.channel.call('reloadWindowDriver', windowId);
}
}
export class WindowDriverChannel implements IServerChannel {
constructor(private driver: IWindowDriver) { }
listen<T>(_: unknown, event: string): Event<T> {
throw new Error(`No event found: ${event}`);
}
call(_: unknown, command: string, arg?: any): Promise<any> {
switch (command) {
case 'click': return this.driver.click(arg[0], arg[1], arg[2]);
case 'doubleClick': return this.driver.doubleClick(arg);
case 'setValue': return this.driver.setValue(arg[0], arg[1]);
case 'getTitle': return this.driver.getTitle();
case 'isActiveElement': return this.driver.isActiveElement(arg);
case 'getElements': return this.driver.getElements(arg[0], arg[1]);
case 'getElementXY': return this.driver.getElementXY(arg[0], arg[1], arg[2]);
case 'typeInEditor': return this.driver.typeInEditor(arg[0], arg[1]);
case 'getTerminalBuffer': return this.driver.getTerminalBuffer(arg);
case 'writeInTerminal': return this.driver.writeInTerminal(arg[0], arg[1]);
}
throw new Error(`Call not found: ${command}`);
}
}
export class WindowDriverChannelClient implements IWindowDriver {
declare readonly _serviceBrand: undefined;
constructor(private channel: IChannel) { }
click(selector: string, xoffset?: number, yoffset?: number): Promise<void> {
return this.channel.call('click', [selector, xoffset, yoffset]);
}
doubleClick(selector: string): Promise<void> {
return this.channel.call('doubleClick', selector);
}
setValue(selector: string, text: string): Promise<void> {
return this.channel.call('setValue', [selector, text]);
}
getTitle(): Promise<string> {
return this.channel.call('getTitle');
}
isActiveElement(selector: string): Promise<boolean> {
return this.channel.call('isActiveElement', selector);
}
getElements(selector: string, recursive: boolean): Promise<IElement[]> {
return this.channel.call('getElements', [selector, recursive]);
}
getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number, y: number }> {
return this.channel.call('getElementXY', [selector, xoffset, yoffset]);
}
typeInEditor(selector: string, text: string): Promise<void> {
return this.channel.call('typeInEditor', [selector, text]);
}
getTerminalBuffer(selector: string): Promise<string[]> {
return this.channel.call('getTerminalBuffer', selector);
}
writeInTerminal(selector: string, text: string): Promise<void> {
return this.channel.call('writeInTerminal', [selector, text]);
}
}
export async function connect(handle: string): Promise<{ client: Client, driver: IDriver }> {
const client = await connectNet(handle, 'driverClient');
const channel = client.getChannel('driver');
const driver = new DriverChannelClient(channel);
return { client, driver };
}

View File

@@ -0,0 +1,269 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { Event } from 'vs/base/common/event';
export interface IEditorModel {
/**
* Emitted when the model is disposed.
*/
readonly onDispose: Event<void>;
/**
* Loads the model.
*/
load(): Promise<IEditorModel>;
/**
* Find out if this model has been disposed.
*/
isDisposed(): boolean;
/**
* Dispose associated resources
*/
dispose(): void;
}
export interface IBaseResourceEditorInput {
/**
* Optional options to use when opening the text input.
*/
options?: ITextEditorOptions;
/**
* Label to show for the diff editor
*/
readonly label?: string;
/**
* Description to show for the diff editor
*/
readonly description?: string;
/**
* Hint to indicate that this input should be treated as a file
* that opens in an editor capable of showing file content.
*
* Without this hint, the editor service will make a guess by
* looking at the scheme of the resource(s).
*/
readonly forceFile?: boolean;
/**
* Hint to indicate that this input should be treated as a
* untitled file.
*
* Without this hint, the editor service will make a guess by
* looking at the scheme of the resource(s).
*/
readonly forceUntitled?: boolean;
}
export interface IResourceEditorInput extends IBaseResourceEditorInput {
/**
* The resource URI of the resource to open.
*/
readonly resource: URI;
/**
* The encoding of the text input if known.
*/
readonly encoding?: string;
/**
* The identifier of the language mode of the text input
* if known to use when displaying the contents.
*/
readonly mode?: string;
}
export enum EditorActivation {
/**
* Activate the editor after it opened. This will automatically restore
* the editor if it is minimized.
*/
ACTIVATE,
/**
* Only restore the editor if it is minimized but do not activate it.
*
* Note: will only work in combination with the `preserveFocus: true` option.
* Otherwise, if focus moves into the editor, it will activate and restore
* automatically.
*/
RESTORE,
/**
* Preserve the current active editor.
*
* Note: will only work in combination with the `preserveFocus: true` option.
* Otherwise, if focus moves into the editor, it will activate and restore
* automatically.
*/
PRESERVE
}
export enum EditorOpenContext {
/**
* Default: the editor is opening via a programmatic call
* to the editor service API.
*/
API,
/**
* Indicates that a user action triggered the opening, e.g.
* via mouse or keyboard use.
*/
USER
}
export interface IEditorOptions {
/**
* Tells the editor to not receive keyboard focus when the editor is being opened.
*
* Will also not activate the group the editor opens in unless the group is already
* the active one. This behaviour can be overridden via the `activation` option.
*/
readonly preserveFocus?: boolean;
/**
* This option is only relevant if an editor is opened into a group that is not active
* already and allows to control if the inactive group should become active, restored
* or preserved.
*
* By default, the editor group will become active unless `preserveFocus` or `inactive`
* is specified.
*/
readonly activation?: EditorActivation;
/**
* Tells the editor to reload the editor input in the editor even if it is identical to the one
* already showing. By default, the editor will not reload the input if it is identical to the
* one showing.
*/
readonly forceReload?: boolean;
/**
* Will reveal the editor if it is already opened and visible in any of the opened editor groups.
*
* Note that this option is just a hint that might be ignored if the user wants to open an editor explicitly
* to the side of another one or into a specific editor group.
*/
readonly revealIfVisible?: boolean;
/**
* Will reveal the editor if it is already opened (even when not visible) in any of the opened editor groups.
*
* Note that this option is just a hint that might be ignored if the user wants to open an editor explicitly
* to the side of another one or into a specific editor group.
*/
readonly revealIfOpened?: boolean;
/**
* An editor that is pinned remains in the editor stack even when another editor is being opened.
* An editor that is not pinned will always get replaced by another editor that is not pinned.
*/
readonly pinned?: boolean;
/**
* An editor that is sticky moves to the beginning of the editors list within the group and will remain
* there unless explicitly closed. Operations such as "Close All" will not close sticky editors.
*/
readonly sticky?: boolean;
/**
* The index in the document stack where to insert the editor into when opening.
*/
readonly index?: number;
/**
* An active editor that is opened will show its contents directly. Set to true to open an editor
* in the background without loading its contents.
*
* Will also not activate the group the editor opens in unless the group is already
* the active one. This behaviour can be overridden via the `activation` option.
*/
readonly inactive?: boolean;
/**
* Will not show an error in case opening the editor fails and thus allows to show a custom error
* message as needed. By default, an error will be presented as notification if opening was not possible.
*/
readonly ignoreError?: boolean;
/**
* Allows to override the editor that should be used to display the input:
* - `undefined`: let the editor decide for itself
* - `false`: disable overrides
* - `string`: specific override by id
*/
readonly override?: false | string;
/**
* A optional hint to signal in which context the editor opens.
*
* If configured to be `EditorOpenContext.USER`, this hint can be
* used in various places to control the experience. For example,
* if the editor to open fails with an error, a notification could
* inform about this in a modal dialog. If the editor opened through
* some background task, the notification would show in the background,
* not as a modal dialog.
*/
readonly context?: EditorOpenContext;
}
export interface ITextEditorSelection {
readonly startLineNumber: number;
readonly startColumn: number;
readonly endLineNumber?: number;
readonly endColumn?: number;
}
export const enum TextEditorSelectionRevealType {
/**
* Option to scroll vertically or horizontally as necessary and reveal a range centered vertically.
*/
Center = 0,
/**
* Option to scroll vertically or horizontally as necessary and reveal a range centered vertically only if it lies outside the viewport.
*/
CenterIfOutsideViewport = 1,
/**
* Option to scroll vertically or horizontally as necessary and reveal a range close to the top of the viewport, but not quite at the top.
*/
NearTop = 2,
/**
* Option to scroll vertically or horizontally as necessary and reveal a range close to the top of the viewport, but not quite at the top.
* Only if it lies outside the viewport
*/
NearTopIfOutsideViewport = 3,
}
export interface ITextEditorOptions extends IEditorOptions {
/**
* Text editor selection.
*/
readonly selection?: ITextEditorSelection;
/**
* Text editor view state.
*/
readonly viewState?: object;
/**
* Option to control the text editor selection reveal type.
* Defaults to TextEditorSelectionRevealType.Center
*/
readonly selectionRevealType?: TextEditorSelectionRevealType;
}

View File

@@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface ICommonEncryptionService {
readonly _serviceBrand: undefined;
encrypt(value: string): Promise<string>;
decrypt(value: string): Promise<string>;
}

View File

@@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ICommonEncryptionService } from 'vs/platform/encryption/common/encryptionService';
export const IEncryptionMainService = createDecorator<IEncryptionMainService>('encryptionMainService');
export interface IEncryptionMainService extends ICommonEncryptionService { }
export interface Encryption {
encrypt(salt: string, value: string): Promise<string>;
decrypt(salt: string, value: string): Promise<string>;
}
export class EncryptionMainService implements ICommonEncryptionService {
declare readonly _serviceBrand: undefined;
constructor(
private machineId: string) {
}
private encryption(): Promise<Encryption> {
return new Promise((resolve, reject) => require(['vscode-encrypt'], resolve, reject));
}
async encrypt(value: string): Promise<string> {
try {
const encryption = await this.encryption();
return encryption.encrypt(this.machineId, value);
} catch (e) {
return value;
}
}
async decrypt(value: string): Promise<string> {
try {
const encryption = await this.encryption();
return encryption.decrypt(this.machineId, value);
} catch (e) {
return value;
}
}
}

View File

@@ -0,0 +1,97 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* A list of command line arguments we support natively.
*/
export interface NativeParsedArgs {
_: string[];
'folder-uri'?: string[]; // undefined or array of 1 or more
'file-uri'?: string[]; // undefined or array of 1 or more
_urls?: string[];
help?: boolean;
version?: boolean;
telemetry?: boolean;
status?: boolean;
wait?: boolean;
waitMarkerFilePath?: string;
diff?: boolean;
add?: boolean;
goto?: boolean;
'new-window'?: boolean;
'unity-launch'?: boolean; // Always open a new window, except if opening the first window or opening a file or folder as part of the launch.
'reuse-window'?: boolean;
locale?: string;
'user-data-dir'?: string;
'prof-startup'?: boolean;
'prof-startup-prefix'?: string;
'prof-append-timers'?: string;
verbose?: boolean;
trace?: boolean;
'trace-category-filter'?: string;
'trace-options'?: string;
'open-devtools'?: boolean;
log?: string;
logExtensionHostCommunication?: boolean;
'extensions-dir'?: string;
'extensions-download-dir'?: string;
'builtin-extensions-dir'?: string;
extensionDevelopmentPath?: string[]; // // undefined or array of 1 or more local paths or URIs
extensionTestsPath?: string; // either a local path or a URI
'inspect-extensions'?: string;
'inspect-brk-extensions'?: string;
debugId?: string;
debugRenderer?: boolean; // whether we expect a debugger (js-debug) to attach to the renderer, incl webviews+webworker
'inspect-search'?: string;
'inspect-brk-search'?: string;
'disable-extensions'?: boolean;
'disable-extension'?: string[]; // undefined or array of 1 or more
'list-extensions'?: boolean;
'show-versions'?: boolean;
'category'?: string;
'install-extension'?: string[]; // undefined or array of 1 or more
'install-builtin-extension'?: string[]; // undefined or array of 1 or more
'uninstall-extension'?: string[]; // undefined or array of 1 or more
'locate-extension'?: string[]; // undefined or array of 1 or more
'enable-proposed-api'?: string[]; // undefined or array of 1 or more
'open-url'?: boolean;
'skip-release-notes'?: boolean;
'disable-telemetry'?: boolean;
'export-default-configuration'?: string;
'install-source'?: string;
'disable-updates'?: boolean;
'disable-crash-reporter'?: boolean;
'crash-reporter-directory'?: string;
'crash-reporter-id'?: string;
'skip-add-to-recently-opened'?: boolean;
'max-memory'?: string;
'file-write'?: boolean;
'file-chmod'?: boolean;
'driver'?: string;
'driver-verbose'?: boolean;
'remote'?: string;
'disable-user-env-probe'?: boolean;
'force'?: boolean;
'do-not-sync'?: boolean;
'force-user-env'?: boolean;
'sync'?: 'on' | 'off';
'__sandbox'?: boolean;
// chromium command line args: https://electronjs.org/docs/all#supported-chrome-command-line-switches
'no-proxy-server'?: boolean;
'proxy-server'?: string;
'proxy-bypass-list'?: string;
'proxy-pac-url'?: string;
'inspect'?: string;
'inspect-brk'?: string;
'js-flags'?: string;
'disable-gpu'?: boolean;
'nolazy'?: boolean;
'force-device-scale-factor'?: string;
'force-renderer-accessibility'?: boolean;
'ignore-certificate-errors'?: boolean;
'allow-insecure-localhost'?: boolean;
'log-net-log'?: string;
}

View File

@@ -0,0 +1,139 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { URI } from 'vs/base/common/uri';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
export const IEnvironmentService = createDecorator<IEnvironmentService>('environmentService');
export const INativeEnvironmentService = createDecorator<INativeEnvironmentService>('nativeEnvironmentService');
export interface IDebugParams {
port: number | null;
break: boolean;
}
export interface IExtensionHostDebugParams extends IDebugParams {
debugId?: string;
}
/**
* A basic environment service that can be used in various processes,
* such as main, renderer and shared process. Use subclasses of this
* service for specific environment.
*/
export interface IEnvironmentService {
readonly _serviceBrand: undefined;
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
// NOTE: KEEP THIS INTERFACE AS SMALL AS POSSIBLE.
//
// AS SUCH:
// - PUT NON-WEB PROPERTIES INTO NATIVE ENVIRONMENT SERVICE
// - PUT WORKBENCH ONLY PROPERTIES INTO WORKBENCH ENVIRONMENT SERVICE
// - PUT ELECTRON-MAIN ONLY PROPERTIES INTO MAIN ENVIRONMENT SERVICE
//
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// --- user roaming data
userRoamingDataHome: URI;
settingsResource: URI;
keybindingsResource: URI;
keyboardLayoutResource: URI;
argvResource: URI;
snippetsHome: URI;
// --- data paths
untitledWorkspacesHome: URI;
globalStorageHome: URI;
workspaceStorageHome: URI;
// --- settings sync
userDataSyncHome: URI;
userDataSyncLogResource: URI;
sync: 'on' | 'off' | undefined;
// --- extension development
debugExtensionHost: IExtensionHostDebugParams;
isExtensionDevelopment: boolean;
disableExtensions: boolean | string[];
extensionDevelopmentLocationURI?: URI[];
extensionTestsLocationURI?: URI;
// --- logging
logsPath: string;
logLevel?: string;
verbose: boolean;
isBuilt: boolean;
// --- telemetry
disableTelemetry: boolean;
telemetryLogResource: URI;
serviceMachineIdResource: URI;
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
// NOTE: KEEP THIS INTERFACE AS SMALL AS POSSIBLE.
//
// AS SUCH:
// - PUT NON-WEB PROPERTIES INTO NATIVE ENVIRONMENT SERVICE
// - PUT WORKBENCH ONLY PROPERTIES INTO WORKBENCH ENVIRONMENT SERVICE
// - PUT ELECTRON-MAIN ONLY PROPERTIES INTO MAIN ENVIRONMENT SERVICE
//
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
}
/**
* A subclass of the `IEnvironmentService` to be used only in native
* environments (Windows, Linux, macOS) but not e.g. web.
*/
export interface INativeEnvironmentService extends IEnvironmentService {
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
// NOTE: KEEP THIS INTERFACE AS SMALL AS POSSIBLE.
//
// AS SUCH:
// - PUT WORKBENCH ONLY PROPERTIES INTO WORKBENCH ENVIRONMENT SERVICE
// - PUT ELECTRON-MAIN ONLY PROPERTIES INTO MAIN ENVIRONMENT SERVICE
//
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// --- CLI Arguments
args: NativeParsedArgs;
// --- paths
appRoot: string;
userHome: URI;
appSettingsHome: URI;
tmpDir: URI;
userDataPath: string;
machineSettingsResource: URI;
installSourcePath: string;
// --- IPC Handles
sharedIPCHandle: string;
// --- Extensions
extensionsPath?: string;
extensionsDownloadPath: string;
builtinExtensionsPath: string;
// --- Smoke test support
driverHandle?: string;
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
// NOTE: KEEP THIS INTERFACE AS SMALL AS POSSIBLE.
//
// AS SUCH:
// - PUT NON-WEB PROPERTIES INTO NATIVE ENVIRONMENT SERVICE
// - PUT WORKBENCH ONLY PROPERTIES INTO WORKBENCH ENVIRONMENT SERVICE
// - PUT ELECTRON-MAIN ONLY PROPERTIES INTO MAIN ENVIRONMENT SERVICE
//
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
}

View File

@@ -0,0 +1,60 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { join } from 'vs/base/common/path';
import { memoize } from 'vs/base/common/decorators';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import { createStaticIPCHandle } from 'vs/base/parts/ipc/node/ipc.net';
import product from 'vs/platform/product/common/product';
export const IEnvironmentMainService = createDecorator<IEnvironmentMainService>('nativeEnvironmentService');
/**
* A subclass of the `INativeEnvironmentService` to be used only in electron-main
* environments.
*/
export interface IEnvironmentMainService extends INativeEnvironmentService {
// --- backup paths
backupHome: string;
backupWorkspacesPath: string;
// --- V8 script cache path
nodeCachedDataDir?: string;
// --- IPC
mainIPCHandle: string;
// --- config
sandbox: boolean;
driverVerbose: boolean;
disableUpdates: boolean;
}
export class EnvironmentMainService extends NativeEnvironmentService {
@memoize
get backupHome(): string { return join(this.userDataPath, 'Backups'); }
@memoize
get backupWorkspacesPath(): string { return join(this.backupHome, 'workspaces.json'); }
@memoize
get mainIPCHandle(): string { return createStaticIPCHandle(this.userDataPath, 'main', product.version); }
@memoize
get sandbox(): boolean { return !!this._args['__sandbox']; }
@memoize
get driverVerbose(): boolean { return !!this._args['driver-verbose']; }
@memoize
get disableUpdates(): boolean { return !!this._args['disable-updates']; }
@memoize
get nodeCachedDataDir(): string | undefined { return process.env['VSCODE_NODE_CACHED_DATA_DIR'] || undefined; }
}

View File

@@ -0,0 +1,321 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as minimist from 'minimist';
import { localize } from 'vs/nls';
import { isWindows } from 'vs/base/common/platform';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
/**
* This code is also used by standalone cli's. Avoid adding any other dependencies.
*/
const helpCategories = {
o: localize('optionsUpperCase', "Options"),
e: localize('extensionsManagement', "Extensions Management"),
t: localize('troubleshooting', "Troubleshooting")
};
export interface Option<OptionType> {
type: OptionType;
alias?: string;
deprecates?: string; // old deprecated id
args?: string | string[];
description?: string;
cat?: keyof typeof helpCategories;
}
export type OptionDescriptions<T> = {
[P in keyof T]: Option<OptionTypeName<T[P]>>;
};
type OptionTypeName<T> =
T extends boolean ? 'boolean' :
T extends string ? 'string' :
T extends string[] ? 'string[]' :
T extends undefined ? 'undefined' :
'unknown';
export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'diff': { type: 'boolean', cat: 'o', alias: 'd', args: ['file', 'file'], description: localize('diff', "Compare two files with each other.") },
'add': { type: 'boolean', cat: 'o', alias: 'a', args: 'folder', description: localize('add', "Add folder(s) to the last active window.") },
'goto': { type: 'boolean', cat: 'o', alias: 'g', args: 'file:line[:character]', description: localize('goto', "Open a file at the path on the specified line and character position.") },
'new-window': { type: 'boolean', cat: 'o', alias: 'n', description: localize('newWindow', "Force to open a new window.") },
'reuse-window': { type: 'boolean', cat: 'o', alias: 'r', description: localize('reuseWindow', "Force to open a file or folder in an already opened window.") },
'folder-uri': { type: 'string[]', cat: 'o', args: 'uri', description: localize('folderUri', "Opens a window with given folder uri(s)") },
'file-uri': { type: 'string[]', cat: 'o', args: 'uri', description: localize('fileUri', "Opens a window with given file uri(s)") },
'wait': { type: 'boolean', cat: 'o', alias: 'w', description: localize('wait', "Wait for the files to be closed before returning.") },
'waitMarkerFilePath': { type: 'string' },
'locale': { type: 'string', cat: 'o', args: 'locale', description: localize('locale', "The locale to use (e.g. en-US or zh-TW).") },
'user-data-dir': { type: 'string', cat: 'o', args: 'dir', description: localize('userDataDir', "Specifies the directory that user data is kept in. Can be used to open multiple distinct instances of Code.") },
'help': { type: 'boolean', cat: 'o', alias: 'h', description: localize('help', "Print usage.") },
'extensions-dir': { type: 'string', deprecates: 'extensionHomePath', cat: 'e', args: 'dir', description: localize('extensionHomePath', "Set the root path for extensions.") },
'extensions-download-dir': { type: 'string' },
'builtin-extensions-dir': { type: 'string' },
'list-extensions': { type: 'boolean', cat: 'e', description: localize('listExtensions', "List the installed extensions.") },
'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extension.") },
'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extension.") },
'install-extension': { type: 'string[]', cat: 'e', args: 'extension-id[@version] | path-to-vsix', description: localize('installExtension', "Installs or updates the extension. Use `--force` argument to avoid prompts. The identifier of an extension is always `${publisher}.${name}`. To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.") },
'uninstall-extension': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('uninstallExtension', "Uninstalls an extension.") },
'enable-proposed-api': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('experimentalApis', "Enables proposed API features for extensions. Can receive one or more extension IDs to enable individually.") },
'version': { type: 'boolean', cat: 't', alias: 'v', description: localize('version', "Print version.") },
'verbose': { type: 'boolean', cat: 't', description: localize('verbose', "Print verbose output (implies --wait).") },
'log': { type: 'string', cat: 't', args: 'level', description: localize('log', "Log level to use. Default is 'info'. Allowed values are 'critical', 'error', 'warn', 'info', 'debug', 'trace', 'off'.") },
'status': { type: 'boolean', alias: 's', cat: 't', description: localize('status', "Print process usage and diagnostics information.") },
'prof-startup': { type: 'boolean', cat: 't', description: localize('prof-startup', "Run CPU profiler during startup") },
'prof-append-timers': { type: 'string' },
'prof-startup-prefix': { type: 'string' },
'disable-extensions': { type: 'boolean', deprecates: 'disableExtensions', cat: 't', description: localize('disableExtensions', "Disable all installed extensions.") },
'disable-extension': { type: 'string[]', cat: 't', args: 'extension-id', description: localize('disableExtension', "Disable an extension.") },
'sync': { type: 'string', cat: 't', description: localize('turn sync', "Turn sync on or off"), args: ['on', 'off'] },
'inspect-extensions': { type: 'string', deprecates: 'debugPluginHost', args: 'port', cat: 't', description: localize('inspect-extensions', "Allow debugging and profiling of extensions. Check the developer tools for the connection URI.") },
'inspect-brk-extensions': { type: 'string', deprecates: 'debugBrkPluginHost', args: 'port', cat: 't', description: localize('inspect-brk-extensions', "Allow debugging and profiling of extensions with the extension host being paused after start. Check the developer tools for the connection URI.") },
'disable-gpu': { type: 'boolean', cat: 't', description: localize('disableGPU', "Disable GPU hardware acceleration.") },
'max-memory': { type: 'string', cat: 't', description: localize('maxMemory', "Max memory size for a window (in Mbytes).") },
'telemetry': { type: 'boolean', cat: 't', description: localize('telemetry', "Shows all telemetry events which VS code collects.") },
'remote': { type: 'string' },
'locate-extension': { type: 'string[]' },
'extensionDevelopmentPath': { type: 'string[]' },
'extensionTestsPath': { type: 'string' },
'debugId': { type: 'string' },
'debugRenderer': { type: 'boolean' },
'inspect-search': { type: 'string', deprecates: 'debugSearch' },
'inspect-brk-search': { type: 'string', deprecates: 'debugBrkSearch' },
'export-default-configuration': { type: 'string' },
'install-source': { type: 'string' },
'driver': { type: 'string' },
'logExtensionHostCommunication': { type: 'boolean' },
'skip-release-notes': { type: 'boolean' },
'disable-telemetry': { type: 'boolean' },
'disable-updates': { type: 'boolean' },
'disable-crash-reporter': { type: 'boolean' },
'crash-reporter-directory': { type: 'string' },
'crash-reporter-id': { type: 'string' },
'disable-user-env-probe': { type: 'boolean' },
'skip-add-to-recently-opened': { type: 'boolean' },
'unity-launch': { type: 'boolean' },
'open-url': { type: 'boolean' },
'file-write': { type: 'boolean' },
'file-chmod': { type: 'boolean' },
'driver-verbose': { type: 'boolean' },
'install-builtin-extension': { type: 'string[]' },
'force': { type: 'boolean' },
'do-not-sync': { type: 'boolean' },
'trace': { type: 'boolean' },
'trace-category-filter': { type: 'string' },
'trace-options': { type: 'string' },
'force-user-env': { type: 'boolean' },
'open-devtools': { type: 'boolean' },
'__sandbox': { type: 'boolean' },
// chromium flags
'no-proxy-server': { type: 'boolean' },
'proxy-server': { type: 'string' },
'proxy-bypass-list': { type: 'string' },
'proxy-pac-url': { type: 'string' },
'js-flags': { type: 'string' }, // chrome js flags
'inspect': { type: 'string' },
'inspect-brk': { type: 'string' },
'nolazy': { type: 'boolean' }, // node inspect
'force-device-scale-factor': { type: 'string' },
'force-renderer-accessibility': { type: 'boolean' },
'ignore-certificate-errors': { type: 'boolean' },
'allow-insecure-localhost': { type: 'boolean' },
'log-net-log': { type: 'string' },
'_urls': { type: 'string[]' },
_: { type: 'string[]' } // main arguments
};
export interface ErrorReporter {
onUnknownOption(id: string): void;
onMultipleValues(id: string, usedValue: string): void;
}
const ignoringReporter: ErrorReporter = {
onUnknownOption: () => { },
onMultipleValues: () => { }
};
export function parseArgs<T>(args: string[], options: OptionDescriptions<T>, errorReporter: ErrorReporter = ignoringReporter): T {
const alias: { [key: string]: string } = {};
const string: string[] = [];
const boolean: string[] = [];
for (let optionId in options) {
if (optionId[0] === '_') {
continue;
}
const o = options[optionId];
if (o.alias) {
alias[optionId] = o.alias;
}
if (o.type === 'string' || o.type === 'string[]') {
string.push(optionId);
if (o.deprecates) {
string.push(o.deprecates);
}
} else if (o.type === 'boolean') {
boolean.push(optionId);
if (o.deprecates) {
boolean.push(o.deprecates);
}
}
}
// remove aliases to avoid confusion
const parsedArgs = minimist(args, { string, boolean, alias });
const cleanedArgs: any = {};
const remainingArgs: any = parsedArgs;
// https://github.com/microsoft/vscode/issues/58177, https://github.com/microsoft/vscode/issues/106617
cleanedArgs._ = parsedArgs._.map(arg => String(arg)).filter(arg => arg.length > 0);
delete remainingArgs._;
for (let optionId in options) {
const o = options[optionId];
if (o.alias) {
delete remainingArgs[o.alias];
}
let val = remainingArgs[optionId];
if (o.deprecates && remainingArgs.hasOwnProperty(o.deprecates)) {
if (!val) {
val = remainingArgs[o.deprecates];
}
delete remainingArgs[o.deprecates];
}
if (typeof val !== 'undefined') {
if (o.type === 'string[]') {
if (val && !Array.isArray(val)) {
val = [val];
}
} else if (o.type === 'string') {
if (Array.isArray(val)) {
val = val.pop(); // take the last
errorReporter.onMultipleValues(optionId, val);
}
}
cleanedArgs[optionId] = val;
}
delete remainingArgs[optionId];
}
for (let key in remainingArgs) {
errorReporter.onUnknownOption(key);
}
return cleanedArgs;
}
function formatUsage(optionId: string, option: Option<any>) {
let args = '';
if (option.args) {
if (Array.isArray(option.args)) {
args = ` <${option.args.join('> <')}>`;
} else {
args = ` <${option.args}>`;
}
}
if (option.alias) {
return `-${option.alias} --${optionId}${args}`;
}
return `--${optionId}${args}`;
}
// exported only for testing
export function formatOptions(options: OptionDescriptions<any>, columns: number): string[] {
let maxLength = 0;
let usageTexts: [string, string][] = [];
for (const optionId in options) {
const o = options[optionId];
const usageText = formatUsage(optionId, o);
maxLength = Math.max(maxLength, usageText.length);
usageTexts.push([usageText, o.description!]);
}
let argLength = maxLength + 2/*left padding*/ + 1/*right padding*/;
if (columns - argLength < 25) {
// Use a condensed version on narrow terminals
return usageTexts.reduce<string[]>((r, ut) => r.concat([` ${ut[0]}`, ` ${ut[1]}`]), []);
}
let descriptionColumns = columns - argLength - 1;
let result: string[] = [];
for (const ut of usageTexts) {
let usage = ut[0];
let wrappedDescription = wrapText(ut[1], descriptionColumns);
let keyPadding = indent(argLength - usage.length - 2/*left padding*/);
result.push(' ' + usage + keyPadding + wrappedDescription[0]);
for (let i = 1; i < wrappedDescription.length; i++) {
result.push(indent(argLength) + wrappedDescription[i]);
}
}
return result;
}
function indent(count: number): string {
return (<any>' ').repeat(count);
}
function wrapText(text: string, columns: number): string[] {
let lines: string[] = [];
while (text.length) {
let index = text.length < columns ? text.length : text.lastIndexOf(' ', columns);
let line = text.slice(0, index).trim();
text = text.slice(index);
lines.push(line);
}
return lines;
}
export function buildHelpMessage(productName: string, executableName: string, version: string, options: OptionDescriptions<any>, isPipeSupported = true): string {
const columns = (process.stdout).isTTY && (process.stdout).columns || 80;
let help = [`${productName} ${version}`];
help.push('');
help.push(`${localize('usage', "Usage")}: ${executableName} [${localize('options', "options")}][${localize('paths', 'paths')}...]`);
help.push('');
if (isPipeSupported) {
if (isWindows) {
help.push(localize('stdinWindows', "To read output from another program, append '-' (e.g. 'echo Hello World | {0} -')", executableName));
} else {
help.push(localize('stdinUnix', "To read from stdin, append '-' (e.g. 'ps aux | grep code | {0} -')", executableName));
}
help.push('');
}
const optionsByCategory: { [P in keyof typeof helpCategories]?: OptionDescriptions<any> } = {};
for (const optionId in options) {
const o = options[optionId];
if (o.description && o.cat) {
let optionsByCat = optionsByCategory[o.cat];
if (!optionsByCat) {
optionsByCategory[o.cat] = optionsByCat = {};
}
optionsByCat[optionId] = o;
}
}
for (let helpCategoryKey in optionsByCategory) {
const key = <keyof typeof helpCategories>helpCategoryKey;
let categoryOptions = optionsByCategory[key];
if (categoryOptions) {
help.push(helpCategories[key]);
help.push(...formatOptions(categoryOptions, columns));
help.push('');
}
}
return help.join('\n');
}
export function buildVersionMessage(version: string | undefined, commit: string | undefined): string {
return `${version || localize('unknownVersion', "Unknown version")}\n${commit || localize('unknownCommit', "Unknown commit")}\n${process.arch}`;
}

View File

@@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { localize } from 'vs/nls';
import { MIN_MAX_MEMORY_SIZE_MB } from 'vs/platform/files/common/files';
import { parseArgs, ErrorReporter, OPTIONS } from 'vs/platform/environment/node/argv';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
function parseAndValidate(cmdLineArgs: string[], reportWarnings: boolean): NativeParsedArgs {
const errorReporter: ErrorReporter = {
onUnknownOption: (id) => {
console.warn(localize('unknownOption', "Warning: '{0}' is not in the list of known options, but still passed to Electron/Chromium.", id));
},
onMultipleValues: (id, val) => {
console.warn(localize('multipleValues', "Option '{0}' is defined more than once. Using value '{1}.'", id, val));
}
};
const args = parseArgs(cmdLineArgs, OPTIONS, reportWarnings ? errorReporter : undefined);
if (args.goto) {
args._.forEach(arg => assert(/^(\w:)?[^:]+(:\d*){0,2}$/.test(arg), localize('gotoValidation', "Arguments in `--goto` mode should be in the format of `FILE(:LINE(:CHARACTER))`.")));
}
if (args['max-memory']) {
assert(parseInt(args['max-memory']) >= MIN_MAX_MEMORY_SIZE_MB, `The max-memory argument cannot be specified lower than ${MIN_MAX_MEMORY_SIZE_MB} MB.`);
}
return args;
}
function stripAppPath(argv: string[]): string[] | undefined {
const index = argv.findIndex(a => !/^-/.test(a));
if (index > -1) {
return [...argv.slice(0, index), ...argv.slice(index + 1)];
}
return undefined;
}
/**
* Use this to parse raw code process.argv such as: `Electron . --verbose --wait`
*/
export function parseMainProcessArgv(processArgv: string[]): NativeParsedArgs {
let [, ...args] = processArgv;
// If dev, remove the first non-option argument: it's the app location
if (process.env['VSCODE_DEV']) {
args = stripAppPath(args) || [];
}
// If called from CLI, don't report warnings as they are already reported.
let reportWarnings = !process.env['VSCODE_CLI'];
return parseAndValidate(args, reportWarnings);
}
/**
* Use this to parse raw code CLI process.argv such as: `Electron cli.js . --verbose --wait`
*/
export function parseCLIProcessArgv(processArgv: string[]): NativeParsedArgs {
let [, , ...args] = processArgv; // remove the first non-option argument: it's always the app location
return parseAndValidate(args, true);
}
export function addArg(argv: string[], ...args: string[]): string[] {
const endOfArgsMarkerIndex = argv.indexOf('--');
if (endOfArgsMarkerIndex === -1) {
argv.push(...args);
} else {
// if the we have an argument "--" (end of argument marker)
// we cannot add arguments at the end. rather, we add
// arguments before the "--" marker.
argv.splice(endOfArgsMarkerIndex, 0, ...args);
}
return argv;
}

View File

@@ -0,0 +1,250 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDebugParams, IExtensionHostDebugParams, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import * as paths from 'vs/base/node/paths';
import * as os from 'os';
import * as path from 'vs/base/common/path';
import * as resources from 'vs/base/common/resources';
import { memoize } from 'vs/base/common/decorators';
import product from 'vs/platform/product/common/product';
import { toLocalISOString } from 'vs/base/common/date';
import { FileAccess } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { createStaticIPCHandle } from 'vs/base/parts/ipc/node/ipc.net';
export class NativeEnvironmentService implements INativeEnvironmentService {
declare readonly _serviceBrand: undefined;
get args(): NativeParsedArgs { return this._args; }
@memoize
get appRoot(): string { return path.dirname(FileAccess.asFileUri('', require).fsPath); }
readonly logsPath: string;
@memoize
get userHome(): URI { return URI.file(os.homedir()); }
@memoize
get userDataPath(): string {
const vscodePortable = process.env['VSCODE_PORTABLE'];
if (vscodePortable) {
return path.join(vscodePortable, 'user-data');
}
return parseUserDataDir(this._args, process);
}
@memoize
get appSettingsHome(): URI { return URI.file(path.join(this.userDataPath, 'User')); }
@memoize
get tmpDir(): URI { return URI.file(os.tmpdir()); }
@memoize
get userRoamingDataHome(): URI { return this.appSettingsHome; }
@memoize
get settingsResource(): URI { return resources.joinPath(this.userRoamingDataHome, 'settings.json'); }
@memoize
get userDataSyncHome(): URI { return resources.joinPath(this.userRoamingDataHome, 'sync'); }
@memoize
get userDataSyncLogResource(): URI { return URI.file(path.join(this.logsPath, 'userDataSync.log')); }
@memoize
get sync(): 'on' | 'off' | undefined { return this.args.sync; }
@memoize
get machineSettingsResource(): URI { return resources.joinPath(URI.file(path.join(this.userDataPath, 'Machine')), 'settings.json'); }
@memoize
get globalStorageHome(): URI { return URI.joinPath(this.appSettingsHome, 'globalStorage'); }
@memoize
get workspaceStorageHome(): URI { return URI.joinPath(this.appSettingsHome, 'workspaceStorage'); }
@memoize
get keybindingsResource(): URI { return resources.joinPath(this.userRoamingDataHome, 'keybindings.json'); }
@memoize
get keyboardLayoutResource(): URI { return resources.joinPath(this.userRoamingDataHome, 'keyboardLayout.json'); }
@memoize
get argvResource(): URI {
const vscodePortable = process.env['VSCODE_PORTABLE'];
if (vscodePortable) {
return URI.file(path.join(vscodePortable, 'argv.json'));
}
return resources.joinPath(this.userHome, product.dataFolderName, 'argv.json');
}
@memoize
get snippetsHome(): URI { return resources.joinPath(this.userRoamingDataHome, 'snippets'); }
@memoize
get isExtensionDevelopment(): boolean { return !!this._args.extensionDevelopmentPath; }
@memoize
get untitledWorkspacesHome(): URI { return URI.file(path.join(this.userDataPath, 'Workspaces')); }
@memoize
get installSourcePath(): string { return path.join(this.userDataPath, 'installSource'); }
@memoize
get builtinExtensionsPath(): string {
const fromArgs = parsePathArg(this._args['builtin-extensions-dir'], process);
if (fromArgs) {
return fromArgs;
} else {
return path.normalize(path.join(FileAccess.asFileUri('', require).fsPath, '..', 'extensions'));
}
}
get extensionsDownloadPath(): string {
const fromArgs = parsePathArg(this._args['extensions-download-dir'], process);
if (fromArgs) {
return fromArgs;
} else {
return path.join(this.userDataPath, 'CachedExtensionVSIXs');
}
}
@memoize
get extensionsPath(): string {
const fromArgs = parsePathArg(this._args['extensions-dir'], process);
if (fromArgs) {
return fromArgs;
}
const vscodeExtensions = process.env['VSCODE_EXTENSIONS'];
if (vscodeExtensions) {
return vscodeExtensions;
}
const vscodePortable = process.env['VSCODE_PORTABLE'];
if (vscodePortable) {
return path.join(vscodePortable, 'extensions');
}
return resources.joinPath(this.userHome, product.dataFolderName, 'extensions').fsPath;
}
@memoize
get extensionDevelopmentLocationURI(): URI[] | undefined {
const s = this._args.extensionDevelopmentPath;
if (Array.isArray(s)) {
return s.map(p => {
if (/^[^:/?#]+?:\/\//.test(p)) {
return URI.parse(p);
}
return URI.file(path.normalize(p));
});
}
return undefined;
}
@memoize
get extensionTestsLocationURI(): URI | undefined {
const s = this._args.extensionTestsPath;
if (s) {
if (/^[^:/?#]+?:\/\//.test(s)) {
return URI.parse(s);
}
return URI.file(path.normalize(s));
}
return undefined;
}
get disableExtensions(): boolean | string[] {
if (this._args['disable-extensions']) {
return true;
}
const disableExtensions = this._args['disable-extension'];
if (disableExtensions) {
if (typeof disableExtensions === 'string') {
return [disableExtensions];
}
if (Array.isArray(disableExtensions) && disableExtensions.length > 0) {
return disableExtensions;
}
}
return false;
}
@memoize
get debugExtensionHost(): IExtensionHostDebugParams { return parseExtensionHostPort(this._args, this.isBuilt); }
get debugRenderer(): boolean { return !!this._args.debugRenderer; }
get isBuilt(): boolean { return !process.env['VSCODE_DEV']; }
get verbose(): boolean { return !!this._args.verbose; }
get logLevel(): string | undefined { return this._args.log; }
@memoize
get sharedIPCHandle(): string { return createStaticIPCHandle(this.userDataPath, 'shared', product.version); }
@memoize
get serviceMachineIdResource(): URI { return resources.joinPath(URI.file(this.userDataPath), 'machineid'); }
get crashReporterId(): string | undefined { return this._args['crash-reporter-id']; }
get crashReporterDirectory(): string | undefined { return this._args['crash-reporter-directory']; }
get driverHandle(): string | undefined { return this._args['driver']; }
@memoize
get telemetryLogResource(): URI { return URI.file(path.join(this.logsPath, 'telemetry.log')); }
get disableTelemetry(): boolean { return !!this._args['disable-telemetry']; }
constructor(protected _args: NativeParsedArgs) {
if (!process.env['VSCODE_LOGS']) {
const key = toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '');
process.env['VSCODE_LOGS'] = path.join(this.userDataPath, 'logs', key);
}
this.logsPath = process.env['VSCODE_LOGS']!;
}
}
export function parseExtensionHostPort(args: NativeParsedArgs, isBuild: boolean): IExtensionHostDebugParams {
return parseDebugPort(args['inspect-extensions'], args['inspect-brk-extensions'], 5870, isBuild, args.debugId);
}
export function parseSearchPort(args: NativeParsedArgs, isBuild: boolean): IDebugParams {
return parseDebugPort(args['inspect-search'], args['inspect-brk-search'], 5876, isBuild);
}
function parseDebugPort(debugArg: string | undefined, debugBrkArg: string | undefined, defaultBuildPort: number, isBuild: boolean, debugId?: string): IExtensionHostDebugParams {
const portStr = debugBrkArg || debugArg;
const port = Number(portStr) || (!isBuild ? defaultBuildPort : null);
const brk = port ? Boolean(!!debugBrkArg) : false;
return { port, break: brk, debugId };
}
export function parsePathArg(arg: string | undefined, process: NodeJS.Process): string | undefined {
if (!arg) {
return undefined;
}
// Determine if the arg is relative or absolute, if relative use the original CWD
// (VSCODE_CWD), not the potentially overridden one (process.cwd()).
const resolved = path.resolve(arg);
if (path.normalize(arg) === resolved) {
return resolved;
}
return path.resolve(process.env['VSCODE_CWD'] || process.cwd(), arg);
}
export function parseUserDataDir(args: NativeParsedArgs, process: NodeJS.Process): string {
return parsePathArg(args['user-data-dir'], process) || path.resolve(paths.getDefaultUserDataPath(process.platform));
}

View File

@@ -0,0 +1,67 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* This code is also used by standalone cli's. Avoid adding dependencies to keep the size of the cli small.
*/
import * as paths from 'vs/base/common/path';
import * as fs from 'fs';
import * as os from 'os';
import { resolveTerminalEncoding } from 'vs/base/node/terminalEncoding';
export function hasStdinWithoutTty() {
try {
return !process.stdin.isTTY; // Via https://twitter.com/MylesBorins/status/782009479382626304
} catch (error) {
// Windows workaround for https://github.com/nodejs/node/issues/11656
}
return false;
}
export function stdinDataListener(durationinMs: number): Promise<boolean> {
return new Promise(resolve => {
const dataListener = () => resolve(true);
// wait for 1s maximum...
setTimeout(() => {
process.stdin.removeListener('data', dataListener);
resolve(false);
}, durationinMs);
// ...but finish early if we detect data
process.stdin.once('data', dataListener);
});
}
export function getStdinFilePath(): string {
return paths.join(os.tmpdir(), `code-stdin-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 3)}.txt`);
}
export async function readFromStdin(targetPath: string, verbose: boolean): Promise<void> {
// open tmp file for writing
const stdinFileStream = fs.createWriteStream(targetPath);
let encoding = await resolveTerminalEncoding(verbose);
const iconv = await import('iconv-lite-umd');
if (!iconv.encodingExists(encoding)) {
console.log(`Unsupported terminal encoding: ${encoding}, falling back to UTF-8.`);
encoding = 'utf8';
}
// Pipe into tmp file using terminals encoding
const decoder = iconv.getDecoder(encoding);
process.stdin.on('data', chunk => stdinFileStream.write(decoder.write(chunk)));
process.stdin.on('end', () => {
const end = decoder.end();
if (typeof end === 'string') {
stdinFileStream.write(end);
}
stdinFileStream.end();
});
process.stdin.on('error', error => stdinFileStream.destroy(error));
process.stdin.on('close', () => stdinFileStream.close());
}

View File

@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* This code is also used by standalone cli's. Avoid adding dependencies to keep the size of the cli small.
*/
import * as path from 'vs/base/common/path';
import * as os from 'os';
import * as fs from 'fs';
export function createWaitMarkerFile(verbose?: boolean): string | undefined {
const randomWaitMarkerPath = path.join(os.tmpdir(), Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10));
try {
fs.writeFileSync(randomWaitMarkerPath, ''); // use built-in fs to avoid dragging in more dependencies
if (verbose) {
console.log(`Marker file for --wait created: ${randomWaitMarkerPath}`);
}
return randomWaitMarkerPath;
} catch (err) {
if (verbose) {
console.error(`Failed to create marker file for --wait: ${err}`);
}
return undefined;
}
}

View File

@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as path from 'vs/base/common/path';
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { parseExtensionHostPort, parseUserDataDir } from 'vs/platform/environment/node/environmentService';
suite('EnvironmentService', () => {
test('parseExtensionHostPort when built', () => {
const parse = (a: string[]) => parseExtensionHostPort(parseArgs(a, OPTIONS), true);
assert.deepEqual(parse([]), { port: null, break: false, debugId: undefined });
assert.deepEqual(parse(['--debugPluginHost']), { port: null, break: false, debugId: undefined });
assert.deepEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined });
assert.deepEqual(parse(['--debugBrkPluginHost']), { port: null, break: false, debugId: undefined });
assert.deepEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined });
assert.deepEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' });
assert.deepEqual(parse(['--inspect-extensions']), { port: null, break: false, debugId: undefined });
assert.deepEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, debugId: undefined });
assert.deepEqual(parse(['--inspect-brk-extensions']), { port: null, break: false, debugId: undefined });
assert.deepEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, debugId: undefined });
assert.deepEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' });
});
test('parseExtensionHostPort when unbuilt', () => {
const parse = (a: string[]) => parseExtensionHostPort(parseArgs(a, OPTIONS), false);
assert.deepEqual(parse([]), { port: 5870, break: false, debugId: undefined });
assert.deepEqual(parse(['--debugPluginHost']), { port: 5870, break: false, debugId: undefined });
assert.deepEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined });
assert.deepEqual(parse(['--debugBrkPluginHost']), { port: 5870, break: false, debugId: undefined });
assert.deepEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined });
assert.deepEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' });
assert.deepEqual(parse(['--inspect-extensions']), { port: 5870, break: false, debugId: undefined });
assert.deepEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, debugId: undefined });
assert.deepEqual(parse(['--inspect-brk-extensions']), { port: 5870, break: false, debugId: undefined });
assert.deepEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, debugId: undefined });
assert.deepEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' });
});
test('userDataPath', () => {
const parse = (a: string[], b: { cwd: () => string, env: { [key: string]: string } }) => parseUserDataDir(parseArgs(a, OPTIONS), <any>b);
assert.equal(parse(['--user-data-dir', './dir'], { cwd: () => '/foo', env: {} }), path.resolve('/foo/dir'),
'should use cwd when --user-data-dir is specified');
assert.equal(parse(['--user-data-dir', './dir'], { cwd: () => '/foo', env: { 'VSCODE_CWD': '/bar' } }), path.resolve('/bar/dir'),
'should use VSCODE_CWD as the cwd when --user-data-dir is specified');
});
// https://github.com/microsoft/vscode/issues/78440
test('careful with boolean file names', function () {
let actual = parseArgs(['-r', 'arg.txt'], OPTIONS);
assert(actual['reuse-window']);
assert.deepEqual(actual._, ['arg.txt']);
actual = parseArgs(['-r', 'true.txt'], OPTIONS);
assert(actual['reuse-window']);
assert.deepEqual(actual._, ['true.txt']);
});
});

View File

@@ -0,0 +1,114 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
const SshProtocolMatcher = /^([^@:]+@)?([^:]+):/;
const SshUrlMatcher = /^([^@:]+@)?([^:]+):(.+)$/;
const AuthorityMatcher = /^([^@]+@)?([^:]+)(:\d+)?$/;
const SecondLevelDomainMatcher = /([^@:.]+\.[^@:.]+)(:\d+)?$/;
const RemoteMatcher = /^\s*url\s*=\s*(.+\S)\s*$/mg;
const AnyButDot = /[^.]/g;
export const AllowedSecondLevelDomains = [
'github.com',
'bitbucket.org',
'visualstudio.com',
'gitlab.com',
'heroku.com',
'azurewebsites.net',
'ibm.com',
'amazon.com',
'amazonaws.com',
'cloudapp.net',
'rhcloud.com',
'google.com',
'azure.com'
];
function stripLowLevelDomains(domain: string): string | null {
const match = domain.match(SecondLevelDomainMatcher);
return match ? match[1] : null;
}
function extractDomain(url: string): string | null {
if (url.indexOf('://') === -1) {
const match = url.match(SshProtocolMatcher);
if (match) {
return stripLowLevelDomains(match[2]);
} else {
return null;
}
}
try {
const uri = URI.parse(url);
if (uri.authority) {
return stripLowLevelDomains(uri.authority);
}
} catch (e) {
// ignore invalid URIs
}
return null;
}
export function getDomainsOfRemotes(text: string, allowedDomains: readonly string[]): string[] {
const domains = new Set<string>();
let match: RegExpExecArray | null;
while (match = RemoteMatcher.exec(text)) {
const domain = extractDomain(match[1]);
if (domain) {
domains.add(domain);
}
}
const allowedDomainsSet = new Set(allowedDomains);
return Array.from(domains)
.map(key => allowedDomainsSet.has(key) ? key : key.replace(AnyButDot, 'a'));
}
function stripPort(authority: string): string | null {
const match = authority.match(AuthorityMatcher);
return match ? match[2] : null;
}
function normalizeRemote(host: string | null, path: string, stripEndingDotGit: boolean): string | null {
if (host && path) {
if (stripEndingDotGit && path.endsWith('.git')) {
path = path.substr(0, path.length - 4);
}
return (path.indexOf('/') === 0) ? `${host}${path}` : `${host}/${path}`;
}
return null;
}
function extractRemote(url: string, stripEndingDotGit: boolean): string | null {
if (url.indexOf('://') === -1) {
const match = url.match(SshUrlMatcher);
if (match) {
return normalizeRemote(match[2], match[3], stripEndingDotGit);
}
}
try {
const uri = URI.parse(url);
if (uri.authority) {
return normalizeRemote(stripPort(uri.authority), uri.path, stripEndingDotGit);
}
} catch (e) {
// ignore invalid URIs
}
return null;
}
export function getRemotes(text: string, stripEndingDotGit: boolean = false): string[] {
const remotes: string[] = [];
let match: RegExpExecArray | null;
while (match = RemoteMatcher.exec(text)) {
const remote = extractRemote(match[1], stripEndingDotGit);
if (remote) {
remotes.push(remote);
}
}
return remotes;
}

View File

@@ -0,0 +1,159 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IExtensionIdentifier, IGlobalExtensionEnablementService, DISABLED_EXTENSIONS_STORAGE_PATH } from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage';
import { isUndefinedOrNull } from 'vs/base/common/types';
export class GlobalExtensionEnablementService extends Disposable implements IGlobalExtensionEnablementService {
declare readonly _serviceBrand: undefined;
private _onDidChangeEnablement = new Emitter<{ readonly extensions: IExtensionIdentifier[], readonly source?: string }>();
readonly onDidChangeEnablement: Event<{ readonly extensions: IExtensionIdentifier[], readonly source?: string }> = this._onDidChangeEnablement.event;
private readonly storageManger: StorageManager;
constructor(
@IStorageService storageService: IStorageService,
) {
super();
this.storageManger = this._register(new StorageManager(storageService));
this._register(this.storageManger.onDidChange(extensions => this._onDidChangeEnablement.fire({ extensions, source: 'storage' })));
}
async enableExtension(extension: IExtensionIdentifier, source?: string): Promise<boolean> {
if (this._removeFromDisabledExtensions(extension)) {
this._onDidChangeEnablement.fire({ extensions: [extension], source });
return true;
}
return false;
}
async disableExtension(extension: IExtensionIdentifier, source?: string): Promise<boolean> {
if (this._addToDisabledExtensions(extension)) {
this._onDidChangeEnablement.fire({ extensions: [extension], source });
return true;
}
return false;
}
getDisabledExtensions(): IExtensionIdentifier[] {
return this._getExtensions(DISABLED_EXTENSIONS_STORAGE_PATH);
}
async getDisabledExtensionsAsync(): Promise<IExtensionIdentifier[]> {
return this.getDisabledExtensions();
}
private _addToDisabledExtensions(identifier: IExtensionIdentifier): boolean {
let disabledExtensions = this.getDisabledExtensions();
if (disabledExtensions.every(e => !areSameExtensions(e, identifier))) {
disabledExtensions.push(identifier);
this._setDisabledExtensions(disabledExtensions);
return true;
}
return false;
}
private _removeFromDisabledExtensions(identifier: IExtensionIdentifier): boolean {
let disabledExtensions = this.getDisabledExtensions();
for (let index = 0; index < disabledExtensions.length; index++) {
const disabledExtension = disabledExtensions[index];
if (areSameExtensions(disabledExtension, identifier)) {
disabledExtensions.splice(index, 1);
this._setDisabledExtensions(disabledExtensions);
return true;
}
}
return false;
}
private _setDisabledExtensions(disabledExtensions: IExtensionIdentifier[]): void {
this._setExtensions(DISABLED_EXTENSIONS_STORAGE_PATH, disabledExtensions);
}
private _getExtensions(storageId: string): IExtensionIdentifier[] {
return this.storageManger.get(storageId, StorageScope.GLOBAL);
}
private _setExtensions(storageId: string, extensions: IExtensionIdentifier[]): void {
this.storageManger.set(storageId, extensions, StorageScope.GLOBAL);
}
}
export class StorageManager extends Disposable {
private storage: { [key: string]: string } = Object.create(null);
private _onDidChange: Emitter<IExtensionIdentifier[]> = this._register(new Emitter<IExtensionIdentifier[]>());
readonly onDidChange: Event<IExtensionIdentifier[]> = this._onDidChange.event;
constructor(private storageService: IStorageService) {
super();
this._register(storageService.onDidChangeStorage(e => this.onDidStorageChange(e)));
}
get(key: string, scope: StorageScope): IExtensionIdentifier[] {
let value: string;
if (scope === StorageScope.GLOBAL) {
if (isUndefinedOrNull(this.storage[key])) {
this.storage[key] = this._get(key, scope);
}
value = this.storage[key];
} else {
value = this._get(key, scope);
}
return JSON.parse(value);
}
set(key: string, value: IExtensionIdentifier[], scope: StorageScope): void {
let newValue: string = JSON.stringify(value.map(({ id, uuid }) => (<IExtensionIdentifier>{ id, uuid })));
const oldValue = this._get(key, scope);
if (oldValue !== newValue) {
if (scope === StorageScope.GLOBAL) {
if (value.length) {
this.storage[key] = newValue;
} else {
delete this.storage[key];
}
}
this._set(key, value.length ? newValue : undefined, scope);
}
}
private onDidStorageChange(workspaceStorageChangeEvent: IWorkspaceStorageChangeEvent): void {
if (workspaceStorageChangeEvent.scope === StorageScope.GLOBAL) {
if (!isUndefinedOrNull(this.storage[workspaceStorageChangeEvent.key])) {
const newValue = this._get(workspaceStorageChangeEvent.key, workspaceStorageChangeEvent.scope);
if (newValue !== this.storage[workspaceStorageChangeEvent.key]) {
const oldValues = this.get(workspaceStorageChangeEvent.key, workspaceStorageChangeEvent.scope);
delete this.storage[workspaceStorageChangeEvent.key];
const newValues = this.get(workspaceStorageChangeEvent.key, workspaceStorageChangeEvent.scope);
const added = oldValues.filter(oldValue => !newValues.some(newValue => areSameExtensions(oldValue, newValue)));
const removed = newValues.filter(newValue => !oldValues.some(oldValue => areSameExtensions(oldValue, newValue)));
if (added.length || removed.length) {
this._onDidChange.fire([...added, ...removed]);
}
}
}
}
}
private _get(key: string, scope: StorageScope): string {
return this.storageService.get(key, scope, '[]');
}
private _set(key: string, value: string | undefined, scope: StorageScope): void {
if (value) {
this.storageService.store(key, value, scope);
} else {
this.storageService.remove(key, scope);
}
}
}

View File

@@ -0,0 +1,815 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getErrorMessage, isPromiseCanceledError, canceled } from 'vs/base/common/errors';
import { StatisticType, IGalleryExtension, IExtensionGalleryService, IGalleryExtensionAsset, IQueryOptions, SortBy, SortOrder, IExtensionIdentifier, IReportedExtension, InstallOperation, ITranslation, IGalleryExtensionVersion, IGalleryExtensionAssets, isIExtensionIdentifier, DefaultIconPath } from 'vs/platform/extensionManagement/common/extensionManagement';
import { getGalleryExtensionId, getGalleryExtensionTelemetryData, adoptToGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { getOrDefault } from 'vs/base/common/objects';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IPager } from 'vs/base/common/paging';
import { IRequestService, asJson, asText } from 'vs/platform/request/common/request';
import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request';
import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { CancellationToken } from 'vs/base/common/cancellation';
import { ILogService } from 'vs/platform/log/common/log';
import { IExtensionManifest } from 'vs/platform/extensions/common/extensions';
import { IFileService } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { IProductService } from 'vs/platform/product/common/productService';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
import { optional } from 'vs/platform/instantiation/common/instantiation';
import { joinPath } from 'vs/base/common/resources';
interface IRawGalleryExtensionFile {
assetType: string;
source: string;
}
interface IRawGalleryExtensionProperty {
key: string;
value: string;
}
interface IRawGalleryExtensionVersion {
version: string;
lastUpdated: string;
assetUri: string;
fallbackAssetUri: string;
files: IRawGalleryExtensionFile[];
properties?: IRawGalleryExtensionProperty[];
}
interface IRawGalleryExtensionStatistics {
statisticName: string;
value: number;
}
interface IRawGalleryExtension {
extensionId: string;
extensionName: string;
displayName: string;
shortDescription: string;
publisher: { displayName: string, publisherId: string, publisherName: string; };
versions: IRawGalleryExtensionVersion[];
statistics: IRawGalleryExtensionStatistics[];
flags: string;
}
interface IRawGalleryQueryResult {
results: {
extensions: IRawGalleryExtension[];
resultMetadata: {
metadataType: string;
metadataItems: {
name: string;
count: number;
}[];
}[]
}[];
}
enum Flags {
None = 0x0,
IncludeVersions = 0x1,
IncludeFiles = 0x2,
IncludeCategoryAndTags = 0x4,
IncludeSharedAccounts = 0x8,
IncludeVersionProperties = 0x10,
ExcludeNonValidated = 0x20,
IncludeInstallationTargets = 0x40,
IncludeAssetUri = 0x80,
IncludeStatistics = 0x100,
IncludeLatestVersionOnly = 0x200,
Unpublished = 0x1000
}
function flagsToString(...flags: Flags[]): string {
return String(flags.reduce((r, f) => r | f, 0));
}
enum FilterType {
Tag = 1,
ExtensionId = 4,
Category = 5,
ExtensionName = 7,
Target = 8,
Featured = 9,
SearchText = 10,
ExcludeWithFlags = 12
}
const AssetType = {
Icon: 'Microsoft.VisualStudio.Services.Icons.Default',
Details: 'Microsoft.VisualStudio.Services.Content.Details',
Changelog: 'Microsoft.VisualStudio.Services.Content.Changelog',
Manifest: 'Microsoft.VisualStudio.Code.Manifest',
VSIX: 'Microsoft.VisualStudio.Services.VSIXPackage',
License: 'Microsoft.VisualStudio.Services.Content.License',
Repository: 'Microsoft.VisualStudio.Services.Links.Source'
};
const PropertyType = {
Dependency: 'Microsoft.VisualStudio.Code.ExtensionDependencies',
ExtensionPack: 'Microsoft.VisualStudio.Code.ExtensionPack',
Engine: 'Microsoft.VisualStudio.Code.Engine',
LocalizedLanguages: 'Microsoft.VisualStudio.Code.LocalizedLanguages',
WebExtension: 'Microsoft.VisualStudio.Code.WebExtension'
};
interface ICriterium {
filterType: FilterType;
value?: string;
}
const DefaultPageSize = 10;
interface IQueryState {
pageNumber: number;
pageSize: number;
sortBy: SortBy;
sortOrder: SortOrder;
flags: Flags;
criteria: ICriterium[];
assetTypes: string[];
}
const DefaultQueryState: IQueryState = {
pageNumber: 1,
pageSize: DefaultPageSize,
sortBy: SortBy.NoneOrRelevance,
sortOrder: SortOrder.Default,
flags: Flags.None,
criteria: [],
assetTypes: []
};
class Query {
constructor(private state = DefaultQueryState) { }
get pageNumber(): number { return this.state.pageNumber; }
get pageSize(): number { return this.state.pageSize; }
get sortBy(): number { return this.state.sortBy; }
get sortOrder(): number { return this.state.sortOrder; }
get flags(): number { return this.state.flags; }
withPage(pageNumber: number, pageSize: number = this.state.pageSize): Query {
return new Query({ ...this.state, pageNumber, pageSize });
}
withFilter(filterType: FilterType, ...values: string[]): Query {
const criteria = [
...this.state.criteria,
...values.length ? values.map(value => ({ filterType, value })) : [{ filterType }]
];
return new Query({ ...this.state, criteria });
}
withSortBy(sortBy: SortBy): Query {
return new Query({ ...this.state, sortBy });
}
withSortOrder(sortOrder: SortOrder): Query {
return new Query({ ...this.state, sortOrder });
}
withFlags(...flags: Flags[]): Query {
return new Query({ ...this.state, flags: flags.reduce<number>((r, f) => r | f, 0) });
}
withAssetTypes(...assetTypes: string[]): Query {
return new Query({ ...this.state, assetTypes });
}
get raw(): any {
const { criteria, pageNumber, pageSize, sortBy, sortOrder, flags, assetTypes } = this.state;
const filters = [{ criteria, pageNumber, pageSize, sortBy, sortOrder }];
return { filters, assetTypes, flags };
}
get searchText(): string {
const criterium = this.state.criteria.filter(criterium => criterium.filterType === FilterType.SearchText)[0];
return criterium && criterium.value ? criterium.value : '';
}
}
function getStatistic(statistics: IRawGalleryExtensionStatistics[], name: string): number {
const result = (statistics || []).filter(s => s.statisticName === name)[0];
return result ? result.value : 0;
}
function getCoreTranslationAssets(version: IRawGalleryExtensionVersion): [string, IGalleryExtensionAsset][] {
const coreTranslationAssetPrefix = 'Microsoft.VisualStudio.Code.Translation.';
const result = version.files.filter(f => f.assetType.indexOf(coreTranslationAssetPrefix) === 0);
return result.reduce<[string, IGalleryExtensionAsset][]>((result, file) => {
const asset = getVersionAsset(version, file.assetType);
if (asset) {
result.push([file.assetType.substring(coreTranslationAssetPrefix.length), asset]);
}
return result;
}, []);
}
function getRepositoryAsset(version: IRawGalleryExtensionVersion): IGalleryExtensionAsset | null {
if (version.properties) {
const results = version.properties.filter(p => p.key === AssetType.Repository);
const gitRegExp = new RegExp('((git|ssh|http(s)?)|(git@[\w.]+))(:(//)?)([\w.@\:/\-~]+)(.git)(/)?');
const uri = results.filter(r => gitRegExp.test(r.value))[0];
return uri ? { uri: uri.value, fallbackUri: uri.value } : null;
}
return getVersionAsset(version, AssetType.Repository);
}
function getDownloadAsset(version: IRawGalleryExtensionVersion): IGalleryExtensionAsset {
return {
uri: `${version.fallbackAssetUri}/${AssetType.VSIX}?redirect=true`,
fallbackUri: `${version.fallbackAssetUri}/${AssetType.VSIX}`
};
}
function getIconAsset(version: IRawGalleryExtensionVersion): IGalleryExtensionAsset {
const asset = getVersionAsset(version, AssetType.Icon);
if (asset) {
return asset;
}
const uri = DefaultIconPath;
return { uri, fallbackUri: uri };
}
function getVersionAsset(version: IRawGalleryExtensionVersion, type: string): IGalleryExtensionAsset | null {
const result = version.files.filter(f => f.assetType === type)[0];
return result ? { uri: `${version.assetUri}/${type}`, fallbackUri: `${version.fallbackAssetUri}/${type}` } : null;
}
function getExtensions(version: IRawGalleryExtensionVersion, property: string): string[] {
const values = version.properties ? version.properties.filter(p => p.key === property) : [];
const value = values.length > 0 && values[0].value;
return value ? value.split(',').map(v => adoptToGalleryExtensionId(v)) : [];
}
function getEngine(version: IRawGalleryExtensionVersion): string {
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.Engine) : [];
return (values.length > 0 && values[0].value) || '';
}
function getLocalizedLanguages(version: IRawGalleryExtensionVersion): string[] {
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.LocalizedLanguages) : [];
const value = (values.length > 0 && values[0].value) || '';
return value ? value.split(',') : [];
}
function getIsPreview(flags: string): boolean {
return flags.indexOf('preview') !== -1;
}
function getIsWebExtension(version: IRawGalleryExtensionVersion): boolean {
const webExtensionProperty = version.properties ? version.properties.find(p => p.key === PropertyType.WebExtension) : undefined;
return !!webExtensionProperty && webExtensionProperty.value === 'true';
}
function getWebResource(version: IRawGalleryExtensionVersion): URI | undefined {
return version.files.some(f => f.assetType.startsWith('Microsoft.VisualStudio.Code.WebResources'))
? joinPath(URI.parse(version.assetUri), 'Microsoft.VisualStudio.Code.WebResources', 'extension')
: undefined;
}
function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGalleryExtensionVersion, index: number, query: Query, querySource?: string): IGalleryExtension {
const assets = <IGalleryExtensionAssets>{
manifest: getVersionAsset(version, AssetType.Manifest),
readme: getVersionAsset(version, AssetType.Details),
changelog: getVersionAsset(version, AssetType.Changelog),
license: getVersionAsset(version, AssetType.License),
repository: getRepositoryAsset(version),
download: getDownloadAsset(version),
icon: getIconAsset(version),
coreTranslations: getCoreTranslationAssets(version)
};
return {
identifier: {
id: getGalleryExtensionId(galleryExtension.publisher.publisherName, galleryExtension.extensionName),
uuid: galleryExtension.extensionId
},
name: galleryExtension.extensionName,
version: version.version,
date: version.lastUpdated,
displayName: galleryExtension.displayName,
publisherId: galleryExtension.publisher.publisherId,
publisher: galleryExtension.publisher.publisherName,
publisherDisplayName: galleryExtension.publisher.displayName,
description: galleryExtension.shortDescription || '',
installCount: getStatistic(galleryExtension.statistics, 'install'),
rating: getStatistic(galleryExtension.statistics, 'averagerating'),
ratingCount: getStatistic(galleryExtension.statistics, 'ratingcount'),
assetUri: URI.parse(version.assetUri),
webResource: getWebResource(version),
assetTypes: version.files.map(({ assetType }) => assetType),
assets,
properties: {
dependencies: getExtensions(version, PropertyType.Dependency),
extensionPack: getExtensions(version, PropertyType.ExtensionPack),
engine: getEngine(version),
localizedLanguages: getLocalizedLanguages(version),
webExtension: getIsWebExtension(version)
},
/* __GDPR__FRAGMENT__
"GalleryExtensionTelemetryData2" : {
"index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"searchText": { "classification": "CustomerContent", "purpose": "FeatureInsight" },
"querySource": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
telemetryData: {
index: ((query.pageNumber - 1) * query.pageSize) + index,
searchText: query.searchText,
querySource
},
preview: getIsPreview(galleryExtension.flags)
};
}
interface IRawExtensionsReport {
malicious: string[];
slow: string[];
}
export class ExtensionGalleryService implements IExtensionGalleryService {
declare readonly _serviceBrand: undefined;
private extensionsGalleryUrl: string | undefined;
private extensionsControlUrl: string | undefined;
private readonly commonHeadersPromise: Promise<{ [key: string]: string; }>;
constructor(
@IRequestService private readonly requestService: IRequestService,
@ILogService private readonly logService: ILogService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IFileService private readonly fileService: IFileService,
@IProductService private readonly productService: IProductService,
@optional(IStorageService) storageService: IStorageService,
) {
const config = productService.extensionsGallery;
this.extensionsGalleryUrl = config && config.serviceUrl;
this.extensionsControlUrl = config && config.controlUrl;
this.commonHeadersPromise = resolveMarketplaceHeaders(productService.version, this.environmentService, this.fileService, storageService);
}
private api(path = ''): string {
return `${this.extensionsGalleryUrl}${path}`;
}
isEnabled(): boolean {
return !!this.extensionsGalleryUrl;
}
async getExtensions(names: string[], token: CancellationToken): Promise<IGalleryExtension[]> {
const result: IGalleryExtension[] = [];
let { total, firstPage: pageResult, getPage } = await this.query({ names, pageSize: names.length }, token);
result.push(...pageResult);
for (let pageIndex = 1; result.length < total; pageIndex++) {
pageResult = await getPage(pageIndex, token);
if (pageResult.length) {
result.push(...pageResult);
} else {
break;
}
}
return result;
}
async getCompatibleExtension(arg1: IExtensionIdentifier | IGalleryExtension, version?: string): Promise<IGalleryExtension | null> {
const extension = await this.getCompatibleExtensionByEngine(arg1, version);
if (extension?.properties.webExtension) {
return extension.webResource ? extension : null;
} else {
return extension;
}
}
private async getCompatibleExtensionByEngine(arg1: IExtensionIdentifier | IGalleryExtension, version?: string): Promise<IGalleryExtension | null> {
const extension: IGalleryExtension | null = isIExtensionIdentifier(arg1) ? null : arg1;
if (extension && extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.version)) {
return extension;
}
const { id, uuid } = extension ? extension.identifier : <IExtensionIdentifier>arg1;
let query = new Query()
.withFlags(Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeFiles, Flags.IncludeVersionProperties)
.withPage(1, 1)
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code');
if (uuid) {
query = query.withFilter(FilterType.ExtensionId, uuid);
} else {
query = query.withFilter(FilterType.ExtensionName, id);
}
const { galleryExtensions } = await this.queryGallery(query, CancellationToken.None);
const [rawExtension] = galleryExtensions;
if (!rawExtension || !rawExtension.versions.length) {
return null;
}
if (version) {
const versionAsset = rawExtension.versions.filter(v => v.version === version)[0];
if (versionAsset) {
const extension = toExtension(rawExtension, versionAsset, 0, query);
if (extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.version)) {
return extension;
}
}
return null;
}
const rawVersion = await this.getLastValidExtensionVersion(rawExtension, rawExtension.versions);
if (rawVersion) {
return toExtension(rawExtension, rawVersion, 0, query);
}
return null;
}
query(token: CancellationToken): Promise<IPager<IGalleryExtension>>;
query(options: IQueryOptions, token: CancellationToken): Promise<IPager<IGalleryExtension>>;
async query(arg1: any, arg2?: any): Promise<IPager<IGalleryExtension>> {
const options: IQueryOptions = CancellationToken.isCancellationToken(arg1) ? {} : arg1;
const token: CancellationToken = CancellationToken.isCancellationToken(arg1) ? arg1 : arg2;
if (!this.isEnabled()) {
throw new Error('No extension gallery service configured.');
}
const type = options.names ? 'ids' : (options.text ? 'text' : 'all');
let text = options.text || '';
const pageSize = getOrDefault(options, o => o.pageSize, 50);
type GalleryServiceQueryClassification = {
type: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
text: { classification: 'CustomerContent', purpose: 'FeatureInsight' };
};
type GalleryServiceQueryEvent = {
type: string;
text: string;
};
this.telemetryService.publicLog2<GalleryServiceQueryEvent, GalleryServiceQueryClassification>('galleryService:query', { type, text });
let query = new Query()
.withFlags(Flags.IncludeLatestVersionOnly, Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeFiles, Flags.IncludeVersionProperties)
.withPage(1, pageSize)
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code');
if (text) {
// Use category filter instead of "category:themes"
text = text.replace(/\bcategory:("([^"]*)"|([^"]\S*))(\s+|\b|$)/g, (_, quotedCategory, category) => {
query = query.withFilter(FilterType.Category, category || quotedCategory);
return '';
});
// Use tag filter instead of "tag:debuggers"
text = text.replace(/\btag:("([^"]*)"|([^"]\S*))(\s+|\b|$)/g, (_, quotedTag, tag) => {
query = query.withFilter(FilterType.Tag, tag || quotedTag);
return '';
});
// Use featured filter
text = text.replace(/\bfeatured(\s+|\b|$)/g, () => {
query = query.withFilter(FilterType.Featured);
return '';
});
text = text.trim();
if (text) {
text = text.length < 200 ? text : text.substring(0, 200);
query = query.withFilter(FilterType.SearchText, text);
}
query = query.withSortBy(SortBy.NoneOrRelevance);
} else if (options.ids) {
query = query.withFilter(FilterType.ExtensionId, ...options.ids);
} else if (options.names) {
query = query.withFilter(FilterType.ExtensionName, ...options.names);
} else {
query = query.withSortBy(SortBy.InstallCount);
}
if (typeof options.sortBy === 'number') {
query = query.withSortBy(options.sortBy);
}
if (typeof options.sortOrder === 'number') {
query = query.withSortOrder(options.sortOrder);
}
const { galleryExtensions, total } = await this.queryGallery(query, token);
const extensions = galleryExtensions.map((e, index) => toExtension(e, e.versions[0], index, query, options.source));
const getPage = async (pageIndex: number, ct: CancellationToken) => {
if (ct.isCancellationRequested) {
throw canceled();
}
const nextPageQuery = query.withPage(pageIndex + 1);
const { galleryExtensions } = await this.queryGallery(nextPageQuery, ct);
return galleryExtensions.map((e, index) => toExtension(e, e.versions[0], index, nextPageQuery, options.source));
};
return { firstPage: extensions, total, pageSize: query.pageSize, getPage } as IPager<IGalleryExtension>;
}
private async queryGallery(query: Query, token: CancellationToken): Promise<{ galleryExtensions: IRawGalleryExtension[], total: number; }> {
if (!this.isEnabled()) {
throw new Error('No extension gallery service configured.');
}
// Always exclude non validated and unpublished extensions
query = query
.withFlags(query.flags, Flags.ExcludeNonValidated)
.withFilter(FilterType.ExcludeWithFlags, flagsToString(Flags.Unpublished));
const commonHeaders = await this.commonHeadersPromise;
const data = JSON.stringify(query.raw);
const headers = {
...commonHeaders,
'Content-Type': 'application/json',
'Accept': 'application/json;api-version=3.0-preview.1',
'Accept-Encoding': 'gzip',
'Content-Length': String(data.length)
};
const context = await this.requestService.request({
type: 'POST',
url: this.api('/extensionquery'),
data,
headers
}, token);
if (context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500) {
return { galleryExtensions: [], total: 0 };
}
const result = await asJson<IRawGalleryQueryResult>(context);
if (result) {
const r = result.results[0];
const galleryExtensions = r.extensions;
const resultCount = r.resultMetadata && r.resultMetadata.filter(m => m.metadataType === 'ResultCount')[0];
const total = resultCount && resultCount.metadataItems.filter(i => i.name === 'TotalCount')[0].count || 0;
return { galleryExtensions, total };
}
return { galleryExtensions: [], total: 0 };
}
async reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise<void> {
if (!this.isEnabled()) {
return undefined;
}
const commonHeaders = await this.commonHeadersPromise;
const headers = { ...commonHeaders, Accept: '*/*;api-version=4.0-preview.1' };
try {
await this.requestService.request({
type: 'POST',
url: this.api(`/publishers/${publisher}/extensions/${name}/${version}/stats?statType=${type}`),
headers
}, CancellationToken.None);
} catch (error) { /* Ignore */ }
}
async download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise<void> {
this.logService.trace('ExtensionGalleryService#download', extension.identifier.id);
const data = getGalleryExtensionTelemetryData(extension);
const startTime = new Date().getTime();
/* __GDPR__
"galleryService:downloadVSIX" : {
"duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"${include}": [
"${GalleryExtensionTelemetryData}"
]
}
*/
const log = (duration: number) => this.telemetryService.publicLog('galleryService:downloadVSIX', { ...data, duration });
const operationParam = operation === InstallOperation.Install ? 'install' : operation === InstallOperation.Update ? 'update' : '';
const downloadAsset = operationParam ? {
uri: `${extension.assets.download.uri}&${operationParam}=true`,
fallbackUri: `${extension.assets.download.fallbackUri}?${operationParam}=true`
} : extension.assets.download;
const context = await this.getAsset(downloadAsset);
await this.fileService.writeFile(location, context.stream);
log(new Date().getTime() - startTime);
}
async getReadme(extension: IGalleryExtension, token: CancellationToken): Promise<string> {
if (extension.assets.readme) {
const context = await this.getAsset(extension.assets.readme, {}, token);
const content = await asText(context);
return content || '';
}
return '';
}
async getManifest(extension: IGalleryExtension, token: CancellationToken): Promise<IExtensionManifest | null> {
if (extension.assets.manifest) {
const context = await this.getAsset(extension.assets.manifest, {}, token);
const text = await asText(context);
return text ? JSON.parse(text) : null;
}
return null;
}
async getCoreTranslation(extension: IGalleryExtension, languageId: string): Promise<ITranslation | null> {
const asset = extension.assets.coreTranslations.filter(t => t[0] === languageId.toUpperCase())[0];
if (asset) {
const context = await this.getAsset(asset[1]);
const text = await asText(context);
return text ? JSON.parse(text) : null;
}
return null;
}
async getChangelog(extension: IGalleryExtension, token: CancellationToken): Promise<string> {
if (extension.assets.changelog) {
const context = await this.getAsset(extension.assets.changelog, {}, token);
const content = await asText(context);
return content || '';
}
return '';
}
async getAllVersions(extension: IGalleryExtension, compatible: boolean): Promise<IGalleryExtensionVersion[]> {
let query = new Query()
.withFlags(Flags.IncludeVersions, Flags.IncludeFiles, Flags.IncludeVersionProperties)
.withPage(1, 1)
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code');
if (extension.identifier.uuid) {
query = query.withFilter(FilterType.ExtensionId, extension.identifier.uuid);
} else {
query = query.withFilter(FilterType.ExtensionName, extension.identifier.id);
}
const result: IGalleryExtensionVersion[] = [];
const { galleryExtensions } = await this.queryGallery(query, CancellationToken.None);
if (galleryExtensions.length) {
if (compatible) {
await Promise.all(galleryExtensions[0].versions.map(async v => {
let engine: string | undefined;
try {
engine = await this.getEngine(v);
} catch (error) { /* Ignore error and skip version */ }
if (engine && isEngineValid(engine, this.productService.version)) {
result.push({ version: v!.version, date: v!.lastUpdated });
}
}));
} else {
result.push(...galleryExtensions[0].versions.map(v => ({ version: v.version, date: v.lastUpdated })));
}
}
return result;
}
private async getAsset(asset: IGalleryExtensionAsset, options: IRequestOptions = {}, token: CancellationToken = CancellationToken.None): Promise<IRequestContext> {
const commonHeaders = await this.commonHeadersPromise;
const baseOptions = { type: 'GET' };
const headers = { ...commonHeaders, ...(options.headers || {}) };
options = { ...options, ...baseOptions, headers };
const url = asset.uri;
const fallbackUrl = asset.fallbackUri;
const firstOptions = { ...options, url };
try {
const context = await this.requestService.request(firstOptions, token);
if (context.res.statusCode === 200) {
return context;
}
const message = await asText(context);
throw new Error(`Expected 200, got back ${context.res.statusCode} instead.\n\n${message}`);
} catch (err) {
if (isPromiseCanceledError(err)) {
throw err;
}
const message = getErrorMessage(err);
type GalleryServiceCDNFallbackClassification = {
url: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
message: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
};
type GalleryServiceCDNFallbackEvent = {
url: string;
message: string;
};
this.telemetryService.publicLog2<GalleryServiceCDNFallbackEvent, GalleryServiceCDNFallbackClassification>('galleryService:cdnFallback', { url, message });
const fallbackOptions = { ...options, url: fallbackUrl };
return this.requestService.request(fallbackOptions, token);
}
}
private async getLastValidExtensionVersion(extension: IRawGalleryExtension, versions: IRawGalleryExtensionVersion[]): Promise<IRawGalleryExtensionVersion | null> {
const version = this.getLastValidExtensionVersionFromProperties(extension, versions);
if (version) {
return version;
}
return this.getLastValidExtensionVersionRecursively(extension, versions);
}
private getLastValidExtensionVersionFromProperties(extension: IRawGalleryExtension, versions: IRawGalleryExtensionVersion[]): IRawGalleryExtensionVersion | null {
for (const version of versions) {
const engine = getEngine(version);
if (!engine) {
return null;
}
if (isEngineValid(engine, this.productService.version)) {
return version;
}
}
return null;
}
private async getEngine(version: IRawGalleryExtensionVersion): Promise<string> {
const engine = getEngine(version);
if (engine) {
return engine;
}
const manifestAsset = getVersionAsset(version, AssetType.Manifest);
if (!manifestAsset) {
throw new Error('Manifest was not found');
}
const headers = { 'Accept-Encoding': 'gzip' };
const context = await this.getAsset(manifestAsset, { headers });
const manifest = await asJson<IExtensionManifest>(context);
if (manifest) {
return manifest.engines.vscode;
}
throw new Error('Error while reading manifest');
}
private async getLastValidExtensionVersionRecursively(extension: IRawGalleryExtension, versions: IRawGalleryExtensionVersion[]): Promise<IRawGalleryExtensionVersion | null> {
if (!versions.length) {
return null;
}
const version = versions[0];
const engine = await this.getEngine(version);
if (!isEngineValid(engine, this.productService.version)) {
return this.getLastValidExtensionVersionRecursively(extension, versions.slice(1));
}
version.properties = version.properties || [];
version.properties.push({ key: PropertyType.Engine, value: engine });
return version;
}
async getExtensionsReport(): Promise<IReportedExtension[]> {
if (!this.isEnabled()) {
throw new Error('No extension gallery service configured.');
}
if (!this.extensionsControlUrl) {
return [];
}
const context = await this.requestService.request({ type: 'GET', url: this.extensionsControlUrl }, CancellationToken.None);
if (context.res.statusCode !== 200) {
throw new Error('Could not get extensions report.');
}
const result = await asJson<IRawExtensionsReport>(context);
const map = new Map<string, IReportedExtension>();
if (result) {
for (const id of result.malicious) {
const ext = map.get(id) || { id: { id }, malicious: true, slow: false };
ext.malicious = true;
map.set(id, ext);
}
}
return [...map.values()];
}
}
export async function resolveMarketplaceHeaders(version: string, environmentService: IEnvironmentService, fileService: IFileService, storageService: {
get: (key: string, scope: StorageScope) => string | undefined,
store: (key: string, value: string, scope: StorageScope) => void
} | undefined): Promise<{ [key: string]: string; }> {
const headers: IHeaders = {
'X-Market-Client-Id': `VSCode ${version}`,
'User-Agent': `VSCode ${version}`
};
const uuid = await getServiceMachineId(environmentService, fileService, storageService);
headers['X-Market-User-Id'] = uuid;
return headers;
}

View File

@@ -0,0 +1,279 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { Event } from 'vs/base/common/event';
import { IPager } from 'vs/base/common/paging';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { URI } from 'vs/base/common/uri';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IExtensionManifest, IExtension, ExtensionType } from 'vs/platform/extensions/common/extensions';
import { FileAccess } from 'vs/base/common/network';
export const EXTENSION_IDENTIFIER_PATTERN = '^([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z0-9A-Z][a-z0-9-A-Z]*)$';
export const EXTENSION_IDENTIFIER_REGEX = new RegExp(EXTENSION_IDENTIFIER_PATTERN);
export interface IGalleryExtensionProperties {
dependencies?: string[];
extensionPack?: string[];
engine?: string;
localizedLanguages?: string[];
webExtension?: boolean;
}
export interface IGalleryExtensionAsset {
uri: string;
fallbackUri: string;
}
export interface IGalleryExtensionAssets {
manifest: IGalleryExtensionAsset | null;
readme: IGalleryExtensionAsset | null;
changelog: IGalleryExtensionAsset | null;
license: IGalleryExtensionAsset | null;
repository: IGalleryExtensionAsset | null;
download: IGalleryExtensionAsset;
icon: IGalleryExtensionAsset;
coreTranslations: [string, IGalleryExtensionAsset][];
}
export function isIExtensionIdentifier(thing: any): thing is IExtensionIdentifier {
return thing
&& typeof thing === 'object'
&& typeof thing.id === 'string'
&& (!thing.uuid || typeof thing.uuid === 'string');
}
/* __GDPR__FRAGMENT__
"ExtensionIdentifier" : {
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"uuid": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
export interface IExtensionIdentifier {
id: string;
uuid?: string;
}
export interface IExtensionIdentifierWithVersion extends IExtensionIdentifier {
id: string;
uuid?: string;
version: string;
}
export interface IGalleryExtensionIdentifier extends IExtensionIdentifier {
uuid: string;
}
export interface IGalleryExtensionVersion {
version: string;
date: string;
}
export interface IGalleryExtension {
name: string;
identifier: IGalleryExtensionIdentifier;
version: string;
date: string;
displayName: string;
publisherId: string;
publisher: string;
publisherDisplayName: string;
description: string;
installCount: number;
rating: number;
ratingCount: number;
assetUri: URI;
assetTypes: string[];
assets: IGalleryExtensionAssets;
properties: IGalleryExtensionProperties;
telemetryData: any;
preview: boolean;
webResource?: URI;
}
export interface IGalleryMetadata {
id: string;
publisherId: string;
publisherDisplayName: string;
}
export interface ILocalExtension extends IExtension {
isMachineScoped: boolean;
publisherId: string | null;
publisherDisplayName: string | null;
}
export const enum SortBy {
NoneOrRelevance = 0,
LastUpdatedDate = 1,
Title = 2,
PublisherName = 3,
InstallCount = 4,
PublishedDate = 5,
AverageRating = 6,
WeightedRating = 12
}
export const enum SortOrder {
Default = 0,
Ascending = 1,
Descending = 2
}
export interface IQueryOptions {
text?: string;
ids?: string[];
names?: string[];
pageSize?: number;
sortBy?: SortBy;
sortOrder?: SortOrder;
source?: string;
}
export const enum StatisticType {
Uninstall = 'uninstall'
}
export interface IReportedExtension {
id: IExtensionIdentifier;
malicious: boolean;
}
export const enum InstallOperation {
None = 0,
Install,
Update
}
export interface ITranslation {
contents: { [key: string]: {} };
}
export const IExtensionGalleryService = createDecorator<IExtensionGalleryService>('extensionGalleryService');
export interface IExtensionGalleryService {
readonly _serviceBrand: undefined;
isEnabled(): boolean;
query(token: CancellationToken): Promise<IPager<IGalleryExtension>>;
query(options: IQueryOptions, token: CancellationToken): Promise<IPager<IGalleryExtension>>;
getExtensions(ids: string[], token: CancellationToken): Promise<IGalleryExtension[]>;
download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise<void>;
reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise<void>;
getReadme(extension: IGalleryExtension, token: CancellationToken): Promise<string>;
getManifest(extension: IGalleryExtension, token: CancellationToken): Promise<IExtensionManifest | null>;
getChangelog(extension: IGalleryExtension, token: CancellationToken): Promise<string>;
getCoreTranslation(extension: IGalleryExtension, languageId: string): Promise<ITranslation | null>;
getAllVersions(extension: IGalleryExtension, compatible: boolean): Promise<IGalleryExtensionVersion[]>;
getExtensionsReport(): Promise<IReportedExtension[]>;
getCompatibleExtension(extension: IGalleryExtension): Promise<IGalleryExtension | null>;
getCompatibleExtension(id: IExtensionIdentifier, version?: string): Promise<IGalleryExtension | null>;
}
export interface InstallExtensionEvent {
identifier: IExtensionIdentifier;
zipPath?: string;
gallery?: IGalleryExtension;
}
export interface DidInstallExtensionEvent {
identifier: IExtensionIdentifier;
operation: InstallOperation;
zipPath?: string;
gallery?: IGalleryExtension;
local?: ILocalExtension;
error?: string;
}
export interface DidUninstallExtensionEvent {
identifier: IExtensionIdentifier;
error?: string;
}
export const INSTALL_ERROR_NOT_SUPPORTED = 'notsupported';
export const INSTALL_ERROR_MALICIOUS = 'malicious';
export const INSTALL_ERROR_INCOMPATIBLE = 'incompatible';
export class ExtensionManagementError extends Error {
constructor(message: string, readonly code: string) {
super(message);
}
}
export type InstallOptions = { isBuiltin?: boolean, isMachineScoped?: boolean };
export const IExtensionManagementService = createDecorator<IExtensionManagementService>('extensionManagementService');
export interface IExtensionManagementService {
readonly _serviceBrand: undefined;
onInstallExtension: Event<InstallExtensionEvent>;
onDidInstallExtension: Event<DidInstallExtensionEvent>;
onUninstallExtension: Event<IExtensionIdentifier>;
onDidUninstallExtension: Event<DidUninstallExtensionEvent>;
zip(extension: ILocalExtension): Promise<URI>;
unzip(zipLocation: URI): Promise<IExtensionIdentifier>;
getManifest(vsix: URI): Promise<IExtensionManifest>;
install(vsix: URI, options?: InstallOptions): Promise<ILocalExtension>;
canInstall(extension: IGalleryExtension): Promise<boolean>;
installFromGallery(extension: IGalleryExtension, options?: InstallOptions): Promise<ILocalExtension>;
uninstall(extension: ILocalExtension, force?: boolean): Promise<void>;
reinstallFromGallery(extension: ILocalExtension): Promise<void>;
getInstalled(type?: ExtensionType): Promise<ILocalExtension[]>;
getExtensionsReport(): Promise<IReportedExtension[]>;
updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension>;
updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise<ILocalExtension>;
}
export const DISABLED_EXTENSIONS_STORAGE_PATH = 'extensionsIdentifiers/disabled';
export const ENABLED_EXTENSIONS_STORAGE_PATH = 'extensionsIdentifiers/enabled';
export const IGlobalExtensionEnablementService = createDecorator<IGlobalExtensionEnablementService>('IGlobalExtensionEnablementService');
export interface IGlobalExtensionEnablementService {
readonly _serviceBrand: undefined;
readonly onDidChangeEnablement: Event<{ readonly extensions: IExtensionIdentifier[], readonly source?: string }>;
getDisabledExtensions(): IExtensionIdentifier[];
enableExtension(extension: IExtensionIdentifier, source?: string): Promise<boolean>;
disableExtension(extension: IExtensionIdentifier, source?: string): Promise<boolean>;
}
export type IConfigBasedExtensionTip = {
readonly extensionId: string,
readonly extensionName: string,
readonly isExtensionPack: boolean,
readonly configName: string,
readonly important: boolean,
};
export type IExecutableBasedExtensionTip = {
readonly extensionId: string,
readonly extensionName: string,
readonly isExtensionPack: boolean,
readonly exeName: string,
readonly exeFriendlyName: string,
readonly windowsPath?: string,
};
export type IWorkspaceTips = { readonly remoteSet: string[]; readonly recommendations: string[]; };
export const IExtensionTipsService = createDecorator<IExtensionTipsService>('IExtensionTipsService');
export interface IExtensionTipsService {
readonly _serviceBrand: undefined;
getConfigBasedTips(folder: URI): Promise<IConfigBasedExtensionTip[]>;
getImportantExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]>;
getOtherExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]>;
getAllWorkspacesTips(): Promise<IWorkspaceTips[]>;
}
export const DefaultIconPath = FileAccess.asBrowserUri('./media/defaultIcon.png', require).toString(true);
export const ExtensionsLabel = localize('extensions', "Extensions");
export const ExtensionsLocalizedLabel = { value: ExtensionsLabel, original: 'Extensions' };
export const ExtensionsChannelId = 'extensions';
export const PreferencesLabel = localize('preferences', "Preferences");
export const PreferencesLocalizedLabel = { value: PreferencesLabel, original: 'Preferences' };

View File

@@ -0,0 +1,164 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, IGalleryExtension, DidUninstallExtensionEvent, IExtensionIdentifier, IGalleryMetadata, IReportedExtension, IExtensionTipsService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { Event } from 'vs/base/common/event';
import { URI, UriComponents } from 'vs/base/common/uri';
import { IURITransformer, DefaultURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc';
import { cloneAndChange } from 'vs/base/common/objects';
import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI {
return URI.revive(transformer ? transformer.transformIncoming(uri) : uri);
}
function transformOutgoingURI(uri: URI, transformer: IURITransformer | null): URI {
return transformer ? transformer.transformOutgoingURI(uri) : uri;
}
function transformIncomingExtension(extension: ILocalExtension, transformer: IURITransformer | null): ILocalExtension {
transformer = transformer ? transformer : DefaultURITransformer;
const manifest = extension.manifest;
const transformed = transformAndReviveIncomingURIs({ ...extension, ...{ manifest: undefined } }, transformer);
return { ...transformed, ...{ manifest } };
}
function transformOutgoingExtension(extension: ILocalExtension, transformer: IURITransformer | null): ILocalExtension {
return transformer ? cloneAndChange(extension, value => value instanceof URI ? transformer.transformOutgoingURI(value) : undefined) : extension;
}
export class ExtensionManagementChannel implements IServerChannel {
onInstallExtension: Event<InstallExtensionEvent>;
onDidInstallExtension: Event<DidInstallExtensionEvent>;
onUninstallExtension: Event<IExtensionIdentifier>;
onDidUninstallExtension: Event<DidUninstallExtensionEvent>;
constructor(private service: IExtensionManagementService, private getUriTransformer: (requestContext: any) => IURITransformer | null) {
this.onInstallExtension = Event.buffer(service.onInstallExtension, true);
this.onDidInstallExtension = Event.buffer(service.onDidInstallExtension, true);
this.onUninstallExtension = Event.buffer(service.onUninstallExtension, true);
this.onDidUninstallExtension = Event.buffer(service.onDidUninstallExtension, true);
}
listen(context: any, event: string): Event<any> {
const uriTransformer = this.getUriTransformer(context);
switch (event) {
case 'onInstallExtension': return this.onInstallExtension;
case 'onDidInstallExtension': return Event.map(this.onDidInstallExtension, i => ({ ...i, local: i.local ? transformOutgoingExtension(i.local, uriTransformer) : i.local }));
case 'onUninstallExtension': return this.onUninstallExtension;
case 'onDidUninstallExtension': return this.onDidUninstallExtension;
}
throw new Error('Invalid listen');
}
call(context: any, command: string, args?: any): Promise<any> {
const uriTransformer: IURITransformer | null = this.getUriTransformer(context);
switch (command) {
case 'zip': return this.service.zip(transformIncomingExtension(args[0], uriTransformer)).then(uri => transformOutgoingURI(uri, uriTransformer));
case 'unzip': return this.service.unzip(transformIncomingURI(args[0], uriTransformer));
case 'install': return this.service.install(transformIncomingURI(args[0], uriTransformer));
case 'getManifest': return this.service.getManifest(transformIncomingURI(args[0], uriTransformer));
case 'canInstall': return this.service.canInstall(args[0]);
case 'installFromGallery': return this.service.installFromGallery(args[0], args[1]);
case 'uninstall': return this.service.uninstall(transformIncomingExtension(args[0], uriTransformer), args[1]);
case 'reinstallFromGallery': return this.service.reinstallFromGallery(transformIncomingExtension(args[0], uriTransformer));
case 'getInstalled': return this.service.getInstalled(args[0]).then(extensions => extensions.map(e => transformOutgoingExtension(e, uriTransformer)));
case 'updateMetadata': return this.service.updateMetadata(transformIncomingExtension(args[0], uriTransformer), args[1]).then(e => transformOutgoingExtension(e, uriTransformer));
case 'updateExtensionScope': return this.service.updateExtensionScope(transformIncomingExtension(args[0], uriTransformer), args[1]).then(e => transformOutgoingExtension(e, uriTransformer));
case 'getExtensionsReport': return this.service.getExtensionsReport();
}
throw new Error('Invalid call');
}
}
export class ExtensionManagementChannelClient implements IExtensionManagementService {
declare readonly _serviceBrand: undefined;
constructor(
private readonly channel: IChannel,
) { }
get onInstallExtension(): Event<InstallExtensionEvent> { return this.channel.listen('onInstallExtension'); }
get onDidInstallExtension(): Event<DidInstallExtensionEvent> { return Event.map(this.channel.listen<DidInstallExtensionEvent>('onDidInstallExtension'), i => ({ ...i, local: i.local ? transformIncomingExtension(i.local, null) : i.local })); }
get onUninstallExtension(): Event<IExtensionIdentifier> { return this.channel.listen('onUninstallExtension'); }
get onDidUninstallExtension(): Event<DidUninstallExtensionEvent> { return this.channel.listen('onDidUninstallExtension'); }
zip(extension: ILocalExtension): Promise<URI> {
return Promise.resolve(this.channel.call('zip', [extension]).then(result => URI.revive(<UriComponents>result)));
}
unzip(zipLocation: URI): Promise<IExtensionIdentifier> {
return Promise.resolve(this.channel.call('unzip', [zipLocation]));
}
install(vsix: URI): Promise<ILocalExtension> {
return Promise.resolve(this.channel.call<ILocalExtension>('install', [vsix])).then(local => transformIncomingExtension(local, null));
}
getManifest(vsix: URI): Promise<IExtensionManifest> {
return Promise.resolve(this.channel.call<IExtensionManifest>('getManifest', [vsix]));
}
async canInstall(extension: IGalleryExtension): Promise<boolean> {
return true;
}
installFromGallery(extension: IGalleryExtension, installOptions?: InstallOptions): Promise<ILocalExtension> {
return Promise.resolve(this.channel.call<ILocalExtension>('installFromGallery', [extension, installOptions])).then(local => transformIncomingExtension(local, null));
}
uninstall(extension: ILocalExtension, force = false): Promise<void> {
return Promise.resolve(this.channel.call('uninstall', [extension!, force]));
}
reinstallFromGallery(extension: ILocalExtension): Promise<void> {
return Promise.resolve(this.channel.call('reinstallFromGallery', [extension]));
}
getInstalled(type: ExtensionType | null = null): Promise<ILocalExtension[]> {
return Promise.resolve(this.channel.call<ILocalExtension[]>('getInstalled', [type]))
.then(extensions => extensions.map(extension => transformIncomingExtension(extension, null)));
}
updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension> {
return Promise.resolve(this.channel.call<ILocalExtension>('updateMetadata', [local, metadata]))
.then(extension => transformIncomingExtension(extension, null));
}
updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise<ILocalExtension> {
return Promise.resolve(this.channel.call<ILocalExtension>('updateExtensionScope', [local, isMachineScoped]))
.then(extension => transformIncomingExtension(extension, null));
}
getExtensionsReport(): Promise<IReportedExtension[]> {
return Promise.resolve(this.channel.call('getExtensionsReport'));
}
}
export class ExtensionTipsChannel implements IServerChannel {
constructor(private service: IExtensionTipsService) {
}
listen(context: any, event: string): Event<any> {
throw new Error('Invalid listen');
}
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'getConfigBasedTips': return this.service.getConfigBasedTips(URI.revive(args[0]));
case 'getImportantExecutableBasedTips': return this.service.getImportantExecutableBasedTips();
case 'getOtherExecutableBasedTips': return this.service.getOtherExecutableBasedTips();
case 'getAllWorkspacesTips': return this.service.getAllWorkspacesTips();
}
throw new Error('Invalid call');
}
}

View File

@@ -0,0 +1,126 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ILocalExtension, IGalleryExtension, IExtensionIdentifier, IReportedExtension, IExtensionIdentifierWithVersion } from 'vs/platform/extensionManagement/common/extensionManagement';
import { compareIgnoreCase } from 'vs/base/common/strings';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
export function areSameExtensions(a: IExtensionIdentifier, b: IExtensionIdentifier): boolean {
if (a.uuid && b.uuid) {
return a.uuid === b.uuid;
}
if (a.id === b.id) {
return true;
}
return compareIgnoreCase(a.id, b.id) === 0;
}
export class ExtensionIdentifierWithVersion implements IExtensionIdentifierWithVersion {
readonly id: string;
readonly uuid?: string;
constructor(
identifier: IExtensionIdentifier,
readonly version: string
) {
this.id = identifier.id;
this.uuid = identifier.uuid;
}
key(): string {
return `${this.id}-${this.version}`;
}
equals(o: any): boolean {
if (!(o instanceof ExtensionIdentifierWithVersion)) {
return false;
}
return areSameExtensions(this, o) && this.version === o.version;
}
}
export function adoptToGalleryExtensionId(id: string): string {
return id.toLocaleLowerCase();
}
export function getGalleryExtensionId(publisher: string, name: string): string {
return `${publisher.toLocaleLowerCase()}.${name.toLocaleLowerCase()}`;
}
export function groupByExtension<T>(extensions: T[], getExtensionIdentifier: (t: T) => IExtensionIdentifier): T[][] {
const byExtension: T[][] = [];
const findGroup = (extension: T) => {
for (const group of byExtension) {
if (group.some(e => areSameExtensions(getExtensionIdentifier(e), getExtensionIdentifier(extension)))) {
return group;
}
}
return null;
};
for (const extension of extensions) {
const group = findGroup(extension);
if (group) {
group.push(extension);
} else {
byExtension.push([extension]);
}
}
return byExtension;
}
export function getLocalExtensionTelemetryData(extension: ILocalExtension): any {
return {
id: extension.identifier.id,
name: extension.manifest.name,
galleryId: null,
publisherId: extension.publisherId,
publisherName: extension.manifest.publisher,
publisherDisplayName: extension.publisherDisplayName,
dependencies: extension.manifest.extensionDependencies && extension.manifest.extensionDependencies.length > 0
};
}
/* __GDPR__FRAGMENT__
"GalleryExtensionTelemetryData" : {
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"name": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"galleryId": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"publisherId": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"publisherName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"publisherDisplayName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"dependencies": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"${include}": [
"${GalleryExtensionTelemetryData2}"
]
}
*/
export function getGalleryExtensionTelemetryData(extension: IGalleryExtension): any {
return {
id: extension.identifier.id,
name: extension.name,
galleryId: extension.identifier.uuid,
publisherId: extension.publisherId,
publisherName: extension.publisher,
publisherDisplayName: extension.publisherDisplayName,
dependencies: !!(extension.properties.dependencies && extension.properties.dependencies.length > 0),
...extension.telemetryData
};
}
export const BetterMergeId = new ExtensionIdentifier('pprice.better-merge');
export function getMaliciousExtensionsSet(report: IReportedExtension[]): Set<string> {
const result = new Set<string>();
for (const extension of report) {
if (extension.malicious) {
result.add(extension.id.id);
}
}
return result;
}

View File

@@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { cloneAndChange } from 'vs/base/common/objects';
import { IExtensionManifest } from 'vs/platform/extensions/common/extensions';
const nlsRegex = /^%([\w\d.-]+)%$/i;
export interface ITranslations {
[key: string]: string;
}
export function localizeManifest(manifest: IExtensionManifest, translations: ITranslations): IExtensionManifest {
const patcher = (value: string) => {
if (typeof value !== 'string') {
return undefined;
}
const match = nlsRegex.exec(value);
if (!match) {
return undefined;
}
return translations[match[1]] || value;
};
return cloneAndChange(manifest, patcher);
}

View File

@@ -0,0 +1,113 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { IProductService, IConfigBasedExtensionTip as IRawConfigBasedExtensionTip } from 'vs/platform/product/common/productService';
import { IFileService } from 'vs/platform/files/common/files';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { IExtensionTipsService, IExecutableBasedExtensionTip, IWorkspaceTips, IConfigBasedExtensionTip } from 'vs/platform/extensionManagement/common/extensionManagement';
import { forEach } from 'vs/base/common/collections';
import { IRequestService, asJson } from 'vs/platform/request/common/request';
import { CancellationToken } from 'vs/base/common/cancellation';
import { ILogService } from 'vs/platform/log/common/log';
import { joinPath } from 'vs/base/common/resources';
import { getDomainsOfRemotes } from 'vs/platform/extensionManagement/common/configRemotes';
import { Disposable } from 'vs/base/common/lifecycle';
export class ExtensionTipsService extends Disposable implements IExtensionTipsService {
_serviceBrand: any;
private readonly allConfigBasedTips: Map<string, IRawConfigBasedExtensionTip> = new Map<string, IRawConfigBasedExtensionTip>();
constructor(
@IFileService protected readonly fileService: IFileService,
@IProductService private readonly productService: IProductService,
@IRequestService private readonly requestService: IRequestService,
@ILogService private readonly logService: ILogService,
) {
super();
if (this.productService.configBasedExtensionTips) {
forEach(this.productService.configBasedExtensionTips, ({ value }) => this.allConfigBasedTips.set(value.configPath, value));
}
}
getConfigBasedTips(folder: URI): Promise<IConfigBasedExtensionTip[]> {
return this.getValidConfigBasedTips(folder);
}
getAllWorkspacesTips(): Promise<IWorkspaceTips[]> {
return this.fetchWorkspacesTips();
}
async getImportantExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
return [];
}
async getOtherExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
return [];
}
private async getValidConfigBasedTips(folder: URI): Promise<IConfigBasedExtensionTip[]> {
const result: IConfigBasedExtensionTip[] = [];
for (const [configPath, tip] of this.allConfigBasedTips) {
try {
const content = await this.fileService.readFile(joinPath(folder, configPath));
const recommendationByRemote: Map<string, IConfigBasedExtensionTip> = new Map<string, IConfigBasedExtensionTip>();
forEach(tip.recommendations, ({ key, value }) => {
if (isNonEmptyArray(value.remotes)) {
for (const remote of value.remotes) {
recommendationByRemote.set(remote, {
extensionId: key,
extensionName: value.name,
configName: tip.configName,
important: !!value.important,
isExtensionPack: !!value.isExtensionPack
});
}
} else {
result.push({
extensionId: key,
extensionName: value.name,
configName: tip.configName,
important: !!value.important,
isExtensionPack: !!value.isExtensionPack
});
}
});
const domains = getDomainsOfRemotes(content.value.toString(), [...recommendationByRemote.keys()]);
for (const domain of domains) {
const remote = recommendationByRemote.get(domain);
if (remote) {
result.push(remote);
}
}
} catch (error) { /* Ignore */ }
}
return result;
}
private async fetchWorkspacesTips(): Promise<IWorkspaceTips[]> {
if (!this.productService.extensionsGallery?.recommendationsUrl) {
return [];
}
try {
const context = await this.requestService.request({ type: 'GET', url: this.productService.extensionsGallery?.recommendationsUrl }, CancellationToken.None);
if (context.res.statusCode !== 200) {
return [];
}
const result = await asJson<{ workspaceRecommendations?: IWorkspaceTips[] }>(context);
if (!result) {
return [];
}
return result.workspaceRecommendations || [];
} catch (error) {
this.logService.error(error);
return [];
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,319 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { basename, join, } from 'vs/base/common/path';
import { IProductService } from 'vs/platform/product/common/productService';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals';
import { IFileService } from 'vs/platform/files/common/files';
import { isWindows } from 'vs/base/common/platform';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { IExecutableBasedExtensionTip, IExtensionManagementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { forEach, IStringDictionary } from 'vs/base/common/collections';
import { IRequestService } from 'vs/platform/request/common/request';
import { ILogService } from 'vs/platform/log/common/log';
import { ExtensionTipsService as BaseExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionTipsService';
import { disposableTimeout, timeout } from 'vs/base/common/async';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
import { localize } from 'vs/nls';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
type ExeExtensionRecommendationsClassification = {
extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
exeName: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
};
type IExeBasedExtensionTips = {
readonly exeFriendlyName: string,
readonly windowsPath?: string,
readonly recommendations: { extensionId: string, extensionName: string, isExtensionPack: boolean }[];
};
const promptedExecutableTipsStorageKey = 'extensionTips/promptedExecutableTips';
const lastPromptedMediumImpExeTimeStorageKey = 'extensionTips/lastPromptedMediumImpExeTime';
export class ExtensionTipsService extends BaseExtensionTipsService {
_serviceBrand: any;
private readonly highImportanceExecutableTips: Map<string, IExeBasedExtensionTips> = new Map<string, IExeBasedExtensionTips>();
private readonly mediumImportanceExecutableTips: Map<string, IExeBasedExtensionTips> = new Map<string, IExeBasedExtensionTips>();
private readonly allOtherExecutableTips: Map<string, IExeBasedExtensionTips> = new Map<string, IExeBasedExtensionTips>();
private highImportanceTipsByExe = new Map<string, IExecutableBasedExtensionTip[]>();
private mediumImportanceTipsByExe = new Map<string, IExecutableBasedExtensionTip[]>();
constructor(
@INativeEnvironmentService private readonly environmentService: INativeEnvironmentService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IStorageService private readonly storageService: IStorageService,
@IExtensionRecommendationNotificationService private readonly extensionRecommendationNotificationService: IExtensionRecommendationNotificationService,
@IFileService fileService: IFileService,
@IProductService productService: IProductService,
@IRequestService requestService: IRequestService,
@ILogService logService: ILogService,
) {
super(fileService, productService, requestService, logService);
if (productService.exeBasedExtensionTips) {
forEach(productService.exeBasedExtensionTips, ({ key, value: exeBasedExtensionTip }) => {
const highImportanceRecommendations: { extensionId: string, extensionName: string, isExtensionPack: boolean }[] = [];
const mediumImportanceRecommendations: { extensionId: string, extensionName: string, isExtensionPack: boolean }[] = [];
const otherRecommendations: { extensionId: string, extensionName: string, isExtensionPack: boolean }[] = [];
forEach(exeBasedExtensionTip.recommendations, ({ key: extensionId, value }) => {
if (value.important) {
if (exeBasedExtensionTip.important) {
highImportanceRecommendations.push({ extensionId, extensionName: value.name, isExtensionPack: !!value.isExtensionPack });
} else {
mediumImportanceRecommendations.push({ extensionId, extensionName: value.name, isExtensionPack: !!value.isExtensionPack });
}
} else {
otherRecommendations.push({ extensionId, extensionName: value.name, isExtensionPack: !!value.isExtensionPack });
}
});
if (highImportanceRecommendations.length) {
this.highImportanceExecutableTips.set(key, { exeFriendlyName: exeBasedExtensionTip.friendlyName, windowsPath: exeBasedExtensionTip.windowsPath, recommendations: highImportanceRecommendations });
}
if (mediumImportanceRecommendations.length) {
this.mediumImportanceExecutableTips.set(key, { exeFriendlyName: exeBasedExtensionTip.friendlyName, windowsPath: exeBasedExtensionTip.windowsPath, recommendations: mediumImportanceRecommendations });
}
if (otherRecommendations.length) {
this.allOtherExecutableTips.set(key, { exeFriendlyName: exeBasedExtensionTip.friendlyName, windowsPath: exeBasedExtensionTip.windowsPath, recommendations: otherRecommendations });
}
});
}
/*
3s has come out to be the good number to fetch and prompt important exe based recommendations
Also fetch important exe based recommendations for reporting telemetry
*/
timeout(3000).then(async () => {
await this.collectTips();
this.promptHighImportanceExeBasedTip();
this.promptMediumImportanceExeBasedTip();
});
}
async getImportantExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
const highImportanceExeTips = await this.getValidExecutableBasedExtensionTips(this.highImportanceExecutableTips);
const mediumImportanceExeTips = await this.getValidExecutableBasedExtensionTips(this.mediumImportanceExecutableTips);
return [...highImportanceExeTips, ...mediumImportanceExeTips];
}
getOtherExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
return this.getValidExecutableBasedExtensionTips(this.allOtherExecutableTips);
}
private async collectTips(): Promise<void> {
const highImportanceExeTips = await this.getValidExecutableBasedExtensionTips(this.highImportanceExecutableTips);
const mediumImportanceExeTips = await this.getValidExecutableBasedExtensionTips(this.mediumImportanceExecutableTips);
const local = await this.extensionManagementService.getInstalled();
this.highImportanceTipsByExe = this.groupImportantTipsByExe(highImportanceExeTips, local);
this.mediumImportanceTipsByExe = this.groupImportantTipsByExe(mediumImportanceExeTips, local);
}
private groupImportantTipsByExe(importantExeBasedTips: IExecutableBasedExtensionTip[], local: ILocalExtension[]): Map<string, IExecutableBasedExtensionTip[]> {
const importantExeBasedRecommendations = new Map<string, IExecutableBasedExtensionTip>();
importantExeBasedTips.forEach(tip => importantExeBasedRecommendations.set(tip.extensionId.toLowerCase(), tip));
const { installed, uninstalled: recommendations } = this.groupByInstalled([...importantExeBasedRecommendations.keys()], local);
/* Log installed and uninstalled exe based recommendations */
for (const extensionId of installed) {
const tip = importantExeBasedRecommendations.get(extensionId);
if (tip) {
this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:alreadyInstalled', { extensionId, exeName: basename(tip.windowsPath!) });
}
}
for (const extensionId of recommendations) {
const tip = importantExeBasedRecommendations.get(extensionId);
if (tip) {
this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:notInstalled', { extensionId, exeName: basename(tip.windowsPath!) });
}
}
const promptedExecutableTips = this.getPromptedExecutableTips();
const tipsByExe = new Map<string, IExecutableBasedExtensionTip[]>();
for (const extensionId of recommendations) {
const tip = importantExeBasedRecommendations.get(extensionId);
if (tip && (!promptedExecutableTips[tip.exeName] || !promptedExecutableTips[tip.exeName].includes(tip.extensionId))) {
let tips = tipsByExe.get(tip.exeName);
if (!tips) {
tips = [];
tipsByExe.set(tip.exeName, tips);
}
tips.push(tip);
}
}
return tipsByExe;
}
/**
* High importance tips are prompted once per restart session
*/
private promptHighImportanceExeBasedTip(): void {
if (this.highImportanceTipsByExe.size === 0) {
return;
}
const [exeName, tips] = [...this.highImportanceTipsByExe.entries()][0];
this.promptExeRecommendations(tips)
.then(result => {
switch (result) {
case RecommendationsNotificationResult.Accepted:
this.addToRecommendedExecutables(tips[0].exeName, tips);
break;
case RecommendationsNotificationResult.Ignored:
this.highImportanceTipsByExe.delete(exeName);
break;
case RecommendationsNotificationResult.TooMany:
// Too many notifications. Schedule the prompt after one hour
const disposable = this._register(disposableTimeout(() => { disposable.dispose(); this.promptHighImportanceExeBasedTip(); }, 60 * 60 * 1000 /* 1 hour */));
break;
}
});
}
/**
* Medium importance tips are prompted once per 7 days
*/
private promptMediumImportanceExeBasedTip(): void {
if (this.mediumImportanceTipsByExe.size === 0) {
return;
}
const lastPromptedMediumExeTime = this.getLastPromptedMediumExeTime();
const timeSinceLastPrompt = Date.now() - lastPromptedMediumExeTime;
const promptInterval = 7 * 24 * 60 * 60 * 1000; // 7 Days
if (timeSinceLastPrompt < promptInterval) {
// Wait until interval and prompt
const disposable = this._register(disposableTimeout(() => { disposable.dispose(); this.promptMediumImportanceExeBasedTip(); }, promptInterval - timeSinceLastPrompt));
return;
}
const [exeName, tips] = [...this.mediumImportanceTipsByExe.entries()][0];
this.promptExeRecommendations(tips)
.then(result => {
switch (result) {
case RecommendationsNotificationResult.Accepted:
// Accepted: Update the last prompted time and caches.
this.updateLastPromptedMediumExeTime(Date.now());
this.mediumImportanceTipsByExe.delete(exeName);
this.addToRecommendedExecutables(tips[0].exeName, tips);
// Schedule the next recommendation for next internval
const disposable1 = this._register(disposableTimeout(() => { disposable1.dispose(); this.promptMediumImportanceExeBasedTip(); }, promptInterval));
break;
case RecommendationsNotificationResult.Ignored:
// Ignored: Remove from the cache and prompt next recommendation
this.mediumImportanceTipsByExe.delete(exeName);
this.promptMediumImportanceExeBasedTip();
break;
case RecommendationsNotificationResult.TooMany:
// Too many notifications. Schedule the prompt after one hour
const disposable2 = this._register(disposableTimeout(() => { disposable2.dispose(); this.promptMediumImportanceExeBasedTip(); }, 60 * 60 * 1000 /* 1 hour */));
break;
}
});
}
private promptExeRecommendations(tips: IExecutableBasedExtensionTip[]): Promise<RecommendationsNotificationResult> {
const extensionIds = tips.map(({ extensionId }) => extensionId.toLowerCase());
const message = localize({ key: 'exeRecommended', comment: ['Placeholder string is the name of the software that is installed.'] }, "You have {0} installed on your system. Do you want to install the recommended extensions for it?", tips[0].exeFriendlyName);
return this.extensionRecommendationNotificationService.promptImportantExtensionsInstallNotification(extensionIds, message, `@exe:"${tips[0].exeName}"`, RecommendationSource.EXE);
}
private getLastPromptedMediumExeTime(): number {
let value = this.storageService.getNumber(lastPromptedMediumImpExeTimeStorageKey, StorageScope.GLOBAL);
if (!value) {
value = Date.now();
this.updateLastPromptedMediumExeTime(value);
}
return value;
}
private updateLastPromptedMediumExeTime(value: number): void {
this.storageService.store(lastPromptedMediumImpExeTimeStorageKey, value, StorageScope.GLOBAL);
}
private getPromptedExecutableTips(): IStringDictionary<string[]> {
return JSON.parse(this.storageService.get(promptedExecutableTipsStorageKey, StorageScope.GLOBAL, '{}'));
}
private addToRecommendedExecutables(exeName: string, tips: IExecutableBasedExtensionTip[]) {
const promptedExecutableTips = this.getPromptedExecutableTips();
promptedExecutableTips[exeName] = tips.map(({ extensionId }) => extensionId.toLowerCase());
this.storageService.store(promptedExecutableTipsStorageKey, JSON.stringify(promptedExecutableTips), StorageScope.GLOBAL);
}
private groupByInstalled(recommendationsToSuggest: string[], local: ILocalExtension[]): { installed: string[], uninstalled: string[] } {
const installed: string[] = [], uninstalled: string[] = [];
const installedExtensionsIds = local.reduce((result, i) => { result.add(i.identifier.id.toLowerCase()); return result; }, new Set<string>());
recommendationsToSuggest.forEach(id => {
if (installedExtensionsIds.has(id.toLowerCase())) {
installed.push(id);
} else {
uninstalled.push(id);
}
});
return { installed, uninstalled };
}
private async getValidExecutableBasedExtensionTips(executableTips: Map<string, IExeBasedExtensionTips>): Promise<IExecutableBasedExtensionTip[]> {
const result: IExecutableBasedExtensionTip[] = [];
const checkedExecutables: Map<string, boolean> = new Map<string, boolean>();
for (const exeName of executableTips.keys()) {
const extensionTip = executableTips.get(exeName);
if (!extensionTip || !isNonEmptyArray(extensionTip.recommendations)) {
continue;
}
const exePaths: string[] = [];
if (isWindows) {
if (extensionTip.windowsPath) {
exePaths.push(extensionTip.windowsPath.replace('%USERPROFILE%', process.env['USERPROFILE']!)
.replace('%ProgramFiles(x86)%', process.env['ProgramFiles(x86)']!)
.replace('%ProgramFiles%', process.env['ProgramFiles']!)
.replace('%APPDATA%', process.env['APPDATA']!)
.replace('%WINDIR%', process.env['WINDIR']!));
}
} else {
exePaths.push(join('/usr/local/bin', exeName));
exePaths.push(join('/usr/bin', exeName));
exePaths.push(join(this.environmentService.userHome.fsPath, exeName));
}
for (const exePath of exePaths) {
let exists = checkedExecutables.get(exePath);
if (exists === undefined) {
exists = await this.fileService.exists(URI.file(exePath));
checkedExecutables.set(exePath, exists);
}
if (exists) {
for (const { extensionId, extensionName, isExtensionPack } of extensionTip.recommendations) {
result.push({
extensionId,
extensionName,
isExtensionPack,
exeName,
exeFriendlyName: extensionTip.exeFriendlyName,
windowsPath: extensionTip.windowsPath,
});
}
}
}
}
return result;
}
}

View File

@@ -0,0 +1,97 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files';
import { IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
import { ExtensionIdentifierWithVersion, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ILogService } from 'vs/platform/log/common/log';
import { generateUuid } from 'vs/base/common/uuid';
import * as semver from 'vs/base/common/semver/semver';
const ExtensionIdVersionRegex = /^([^.]+\..+)-(\d+\.\d+\.\d+)$/;
export class ExtensionsDownloader extends Disposable {
private readonly extensionsDownloadDir: URI;
private readonly cache: number;
private readonly cleanUpPromise: Promise<void>;
constructor(
@INativeEnvironmentService environmentService: INativeEnvironmentService,
@IFileService private readonly fileService: IFileService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@ILogService private readonly logService: ILogService,
) {
super();
this.extensionsDownloadDir = URI.file(environmentService.extensionsDownloadPath);
this.cache = 20; // Cache 20 downloads
this.cleanUpPromise = this.cleanUp();
}
async downloadExtension(extension: IGalleryExtension, operation: InstallOperation): Promise<URI> {
await this.cleanUpPromise;
const location = joinPath(this.extensionsDownloadDir, this.getName(extension));
await this.download(extension, location, operation);
return location;
}
async delete(location: URI): Promise<void> {
// noop as caching is enabled always
}
private async download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise<void> {
if (!await this.fileService.exists(location)) {
await this.extensionGalleryService.download(extension, location, operation);
}
}
private async cleanUp(): Promise<void> {
try {
if (!(await this.fileService.exists(this.extensionsDownloadDir))) {
this.logService.trace('Extension VSIX downlads cache dir does not exist');
return;
}
const folderStat = await this.fileService.resolve(this.extensionsDownloadDir, { resolveMetadata: true });
if (folderStat.children) {
const toDelete: URI[] = [];
const all: [ExtensionIdentifierWithVersion, IFileStatWithMetadata][] = [];
for (const stat of folderStat.children) {
const extension = this.parse(stat.name);
if (extension) {
all.push([extension, stat]);
}
}
const byExtension = groupByExtension(all, ([extension]) => extension);
const distinct: IFileStatWithMetadata[] = [];
for (const p of byExtension) {
p.sort((a, b) => semver.rcompare(a[0].version, b[0].version));
toDelete.push(...p.slice(1).map(e => e[1].resource)); // Delete outdated extensions
distinct.push(p[0][1]);
}
distinct.sort((a, b) => a.mtime - b.mtime); // sort by modified time
toDelete.push(...distinct.slice(0, Math.max(0, distinct.length - this.cache)).map(s => s.resource)); // Retain minimum cacheSize and delete the rest
await Promise.all(toDelete.map(resource => {
this.logService.trace('Deleting vsix from cache', resource.path);
return this.fileService.del(resource);
}));
}
} catch (e) {
this.logService.error(e);
}
}
private getName(extension: IGalleryExtension): string {
return this.cache ? new ExtensionIdentifierWithVersion(extension.identifier, extension.version).key().toLowerCase() : generateUuid();
}
private parse(name: string): ExtensionIdentifierWithVersion | null {
const matches = ExtensionIdVersionRegex.exec(name);
return matches && matches[1] && matches[2] ? new ExtensionIdentifierWithVersion({ id: matches[1] }, matches[2]) : null;
}
}

View File

@@ -0,0 +1,135 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ILogService } from 'vs/platform/log/common/log';
import { fork, ChildProcess } from 'child_process';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { join } from 'vs/base/common/path';
import { Limiter } from 'vs/base/common/async';
import { Event } from 'vs/base/common/event';
import { Schemas } from 'vs/base/common/network';
import { rimraf } from 'vs/base/node/pfs';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
export class ExtensionsLifecycle extends Disposable {
private processesLimiter: Limiter<void> = new Limiter(5); // Run max 5 processes in parallel
constructor(
@IEnvironmentService private environmentService: IEnvironmentService,
@ILogService private readonly logService: ILogService
) {
super();
}
async postUninstall(extension: ILocalExtension): Promise<void> {
const script = this.parseScript(extension, 'uninstall');
if (script) {
this.logService.info(extension.identifier.id, extension.manifest.version, `Running post uninstall script`);
await this.processesLimiter.queue(() =>
this.runLifecycleHook(script.script, 'uninstall', script.args, true, extension)
.then(() => this.logService.info(extension.identifier.id, extension.manifest.version, `Finished running post uninstall script`), err => this.logService.error(extension.identifier.id, extension.manifest.version, `Failed to run post uninstall script: ${err}`)));
}
return rimraf(this.getExtensionStoragePath(extension)).then(undefined, e => this.logService.error('Error while removing extension storage path', e));
}
private parseScript(extension: ILocalExtension, type: string): { script: string, args: string[] } | null {
const scriptKey = `vscode:${type}`;
if (extension.location.scheme === Schemas.file && extension.manifest && extension.manifest['scripts'] && typeof extension.manifest['scripts'][scriptKey] === 'string') {
const script = (<string>extension.manifest['scripts'][scriptKey]).split(' ');
if (script.length < 2 || script[0] !== 'node' || !script[1]) {
this.logService.warn(extension.identifier.id, extension.manifest.version, `${scriptKey} should be a node script`);
return null;
}
return { script: join(extension.location.fsPath, script[1]), args: script.slice(2) || [] };
}
return null;
}
private runLifecycleHook(lifecycleHook: string, lifecycleType: string, args: string[], timeout: boolean, extension: ILocalExtension): Promise<void> {
return new Promise<void>((c, e) => {
const extensionLifecycleProcess = this.start(lifecycleHook, lifecycleType, args, extension);
let timeoutHandler: any;
const onexit = (error?: string) => {
if (timeoutHandler) {
clearTimeout(timeoutHandler);
timeoutHandler = null;
}
if (error) {
e(error);
} else {
c(undefined);
}
};
// on error
extensionLifecycleProcess.on('error', (err) => {
onexit(toErrorMessage(err) || 'Unknown');
});
// on exit
extensionLifecycleProcess.on('exit', (code: number, signal: string) => {
onexit(code ? `post-${lifecycleType} process exited with code ${code}` : undefined);
});
if (timeout) {
// timeout: kill process after waiting for 5s
timeoutHandler = setTimeout(() => {
timeoutHandler = null;
extensionLifecycleProcess.kill();
e('timed out');
}, 5000);
}
});
}
private start(uninstallHook: string, lifecycleType: string, args: string[], extension: ILocalExtension): ChildProcess {
const opts = {
silent: true,
execArgv: undefined
};
const extensionUninstallProcess = fork(uninstallHook, [`--type=extension-post-${lifecycleType}`, ...args], opts);
// Catch all output coming from the process
type Output = { data: string, format: string[] };
extensionUninstallProcess.stdout!.setEncoding('utf8');
extensionUninstallProcess.stderr!.setEncoding('utf8');
const onStdout = Event.fromNodeEventEmitter<string>(extensionUninstallProcess.stdout!, 'data');
const onStderr = Event.fromNodeEventEmitter<string>(extensionUninstallProcess.stderr!, 'data');
// Log output
onStdout(data => this.logService.info(extension.identifier.id, extension.manifest.version, `post-${lifecycleType}`, data));
onStderr(data => this.logService.error(extension.identifier.id, extension.manifest.version, `post-${lifecycleType}`, 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 = Event.debounce<Output>(onOutput, (r, o) => {
return r
? { data: r.data + o.data, format: [...r.format, ...o.format] }
: { data: o.data, format: o.format };
}, 100);
// Print out output
onDebouncedOutput(data => {
console.group(extension.identifier.id);
console.log(data.data, ...data.format);
console.groupEnd();
});
return extensionUninstallProcess;
}
private getExtensionStoragePath(extension: ILocalExtension): string {
return join(this.environmentService.globalStorageHome.fsPath, extension.identifier.id.toLowerCase());
}
}

View File

@@ -0,0 +1,766 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as path from 'vs/base/common/path';
import * as pfs from 'vs/base/node/pfs';
import { toDisposable, Disposable } from 'vs/base/common/lifecycle';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { zip, IFile } from 'vs/base/node/zip';
import {
IExtensionManagementService, IExtensionGalleryService, ILocalExtension,
IGalleryExtension, IGalleryMetadata,
InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent,
StatisticType,
IExtensionIdentifier,
IReportedExtension,
InstallOperation,
INSTALL_ERROR_MALICIOUS,
INSTALL_ERROR_INCOMPATIBLE,
ExtensionManagementError,
InstallOptions
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions, getGalleryExtensionId, getMaliciousExtensionsSet, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, ExtensionIdentifierWithVersion } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { createCancelablePromise, CancelablePromise } from 'vs/base/common/async';
import { Event, Emitter } from 'vs/base/common/event';
import * as semver from 'vs/base/common/semver/semver';
import { URI } from 'vs/base/common/uri';
import product from 'vs/platform/product/common/product';
import { isMacintosh } from 'vs/base/common/platform';
import { ILogService } from 'vs/platform/log/common/log';
import { ExtensionsManifestCache } from 'vs/platform/extensionManagement/node/extensionsManifestCache';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator';
import { joinPath } from 'vs/base/common/resources';
import { generateUuid } from 'vs/base/common/uuid';
import { IDownloadService } from 'vs/platform/download/common/download';
import { optional, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Schemas } from 'vs/base/common/network';
import { CancellationToken } from 'vs/base/common/cancellation';
import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil';
import { IExtensionManifest, ExtensionType } from 'vs/platform/extensions/common/extensions';
import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader';
import { ExtensionsScanner, ILocalExtensionManifest, IMetadata } from 'vs/platform/extensionManagement/node/extensionsScanner';
import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle';
const INSTALL_ERROR_UNSET_UNINSTALLED = 'unsetUninstalled';
const INSTALL_ERROR_DOWNLOADING = 'downloading';
const INSTALL_ERROR_VALIDATING = 'validating';
const INSTALL_ERROR_LOCAL = 'local';
const ERROR_UNKNOWN = 'unknown';
interface InstallableExtension {
zipPath: string;
identifierWithVersion: ExtensionIdentifierWithVersion;
metadata?: IMetadata;
}
export class ExtensionManagementService extends Disposable implements IExtensionManagementService {
declare readonly _serviceBrand: undefined;
private readonly extensionsScanner: ExtensionsScanner;
private reportedExtensions: Promise<IReportedExtension[]> | undefined;
private lastReportTimestamp = 0;
private readonly installingExtensions = new Map<string, CancelablePromise<ILocalExtension>>();
private readonly uninstallingExtensions: Map<string, CancelablePromise<void>> = new Map<string, CancelablePromise<void>>();
private readonly manifestCache: ExtensionsManifestCache;
private readonly extensionsDownloader: ExtensionsDownloader;
private readonly _onInstallExtension = this._register(new Emitter<InstallExtensionEvent>());
readonly onInstallExtension: Event<InstallExtensionEvent> = this._onInstallExtension.event;
private readonly _onDidInstallExtension = this._register(new Emitter<DidInstallExtensionEvent>());
readonly onDidInstallExtension: Event<DidInstallExtensionEvent> = this._onDidInstallExtension.event;
private readonly _onUninstallExtension = this._register(new Emitter<IExtensionIdentifier>());
readonly onUninstallExtension: Event<IExtensionIdentifier> = this._onUninstallExtension.event;
private _onDidUninstallExtension = this._register(new Emitter<DidUninstallExtensionEvent>());
onDidUninstallExtension: Event<DidUninstallExtensionEvent> = this._onDidUninstallExtension.event;
constructor(
@INativeEnvironmentService private readonly environmentService: INativeEnvironmentService,
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
@ILogService private readonly logService: ILogService,
@optional(IDownloadService) private downloadService: IDownloadService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IInstantiationService instantiationService: IInstantiationService,
) {
super();
const extensionLifecycle = this._register(instantiationService.createInstance(ExtensionsLifecycle));
this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner, extension => extensionLifecycle.postUninstall(extension)));
this.manifestCache = this._register(new ExtensionsManifestCache(environmentService, this));
this.extensionsDownloader = this._register(instantiationService.createInstance(ExtensionsDownloader));
this._register(toDisposable(() => {
this.installingExtensions.forEach(promise => promise.cancel());
this.uninstallingExtensions.forEach(promise => promise.cancel());
this.installingExtensions.clear();
this.uninstallingExtensions.clear();
}));
}
async zip(extension: ILocalExtension): Promise<URI> {
this.logService.trace('ExtensionManagementService#zip', extension.identifier.id);
const files = await this.collectFiles(extension);
const location = await zip(joinPath(this.environmentService.tmpDir, generateUuid()).fsPath, files);
return URI.file(location);
}
async unzip(zipLocation: URI): Promise<IExtensionIdentifier> {
this.logService.trace('ExtensionManagementService#unzip', zipLocation.toString());
const local = await this.install(zipLocation);
return local.identifier;
}
async getManifest(vsix: URI): Promise<IExtensionManifest> {
const downloadLocation = await this.downloadVsix(vsix);
const zipPath = path.resolve(downloadLocation.fsPath);
return getManifest(zipPath);
}
private async collectFiles(extension: ILocalExtension): Promise<IFile[]> {
const collectFilesFromDirectory = async (dir: string): Promise<string[]> => {
let entries = await pfs.readdir(dir);
entries = entries.map(e => path.join(dir, e));
const stats = await Promise.all(entries.map(e => pfs.stat(e)));
let promise: Promise<string[]> = Promise.resolve([]);
stats.forEach((stat, index) => {
const entry = entries[index];
if (stat.isFile()) {
promise = promise.then(result => ([...result, entry]));
}
if (stat.isDirectory()) {
promise = promise
.then(result => collectFilesFromDirectory(entry)
.then(files => ([...result, ...files])));
}
});
return promise;
};
const files = await collectFilesFromDirectory(extension.location.fsPath);
return files.map(f => (<IFile>{ path: `extension/${path.relative(extension.location.fsPath, f)}`, localPath: f }));
}
async install(vsix: URI, options: InstallOptions = {}): Promise<ILocalExtension> {
this.logService.trace('ExtensionManagementService#install', vsix.toString());
return createCancelablePromise(async token => {
const downloadLocation = await this.downloadVsix(vsix);
const zipPath = path.resolve(downloadLocation.fsPath);
const manifest = await getManifest(zipPath);
const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };
let operation: InstallOperation = InstallOperation.Install;
if (manifest.engines && manifest.engines.vscode && !isEngineValid(manifest.engines.vscode, product.version)) {
throw new Error(nls.localize('incompatible', "Unable to install extension '{0}' as it is not compatible with VS Code '{1}'.", identifier.id, product.version));
}
const identifierWithVersion = new ExtensionIdentifierWithVersion(identifier, manifest.version);
const installedExtensions = await this.getInstalled(ExtensionType.User);
const existing = installedExtensions.find(i => areSameExtensions(identifier, i.identifier));
if (existing) {
options.isMachineScoped = options.isMachineScoped || existing.isMachineScoped;
options.isBuiltin = options.isBuiltin || existing.isBuiltin;
operation = InstallOperation.Update;
if (identifierWithVersion.equals(new ExtensionIdentifierWithVersion(existing.identifier, existing.manifest.version))) {
try {
await this.extensionsScanner.removeExtension(existing, 'existing');
} catch (e) {
throw new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", manifest.displayName || manifest.name));
}
} else if (semver.gt(existing.manifest.version, manifest.version)) {
await this.uninstallExtension(existing);
}
} else {
// Remove the extension with same version if it is already uninstalled.
// Installing a VSIX extension shall replace the existing extension always.
const existing = await this.unsetUninstalledAndGetLocal(identifierWithVersion);
if (existing) {
try {
await this.extensionsScanner.removeExtension(existing, 'existing');
} catch (e) {
throw new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", manifest.displayName || manifest.name));
}
}
}
this.logService.info('Installing the extension:', identifier.id);
this._onInstallExtension.fire({ identifier, zipPath });
let metadata: IGalleryMetadata | undefined;
try {
metadata = await this.getGalleryMetadata(getGalleryExtensionId(manifest.publisher, manifest.name));
} catch (e) { /* Ignore */ }
try {
const local = await this.installFromZipPath(identifierWithVersion, zipPath, { ...(metadata || {}), ...options }, options, operation, token);
this.logService.info('Successfully installed the extension:', identifier.id);
return local;
} catch (e) {
this.logService.error('Failed to install the extension:', identifier.id, e.message);
throw e;
}
});
}
private async downloadVsix(vsix: URI): Promise<URI> {
if (vsix.scheme === Schemas.file) {
return vsix;
}
if (!this.downloadService) {
throw new Error('Download service is not available');
}
const downloadedLocation = joinPath(this.environmentService.tmpDir, generateUuid());
await this.downloadService.download(vsix, downloadedLocation);
return downloadedLocation;
}
private async installFromZipPath(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, metadata: IMetadata | undefined, options: InstallOptions, operation: InstallOperation, token: CancellationToken): Promise<ILocalExtension> {
try {
const local = await this.installExtension({ zipPath, identifierWithVersion, metadata }, token);
try {
await this.installDependenciesAndPackExtensions(local, undefined, options);
} catch (error) {
if (isNonEmptyArray(local.manifest.extensionDependencies)) {
this.logService.warn(`Cannot install dependencies of extension:`, local.identifier.id, error.message);
}
if (isNonEmptyArray(local.manifest.extensionPack)) {
this.logService.warn(`Cannot install packed extensions of extension:`, local.identifier.id, error.message);
}
}
this._onDidInstallExtension.fire({ identifier: identifierWithVersion, zipPath, local, operation });
return local;
} catch (error) {
this._onDidInstallExtension.fire({ identifier: identifierWithVersion, zipPath, operation, error });
throw error;
}
}
async canInstall(extension: IGalleryExtension): Promise<boolean> {
return true;
}
async installFromGallery(extension: IGalleryExtension, options: InstallOptions = {}): Promise<ILocalExtension> {
if (!this.galleryService.isEnabled()) {
throw new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"));
}
try {
extension = await this.checkAndGetCompatibleVersion(extension);
} catch (error) {
const errorCode = error && (<ExtensionManagementError>error).code ? (<ExtensionManagementError>error).code : ERROR_UNKNOWN;
this.logService.error(`Failed to install extension:`, extension.identifier.id, error ? error.message : errorCode);
this.reportTelemetry(this.getTelemetryEvent(InstallOperation.Install), getGalleryExtensionTelemetryData(extension), undefined, error);
if (error instanceof Error) {
error.name = errorCode;
}
throw error;
}
const key = new ExtensionIdentifierWithVersion(extension.identifier, extension.version).key();
let cancellablePromise = this.installingExtensions.get(key);
if (!cancellablePromise) {
cancellablePromise = createCancelablePromise(token => this.doInstallFromGallery(extension, options, token));
this.installingExtensions.set(key, cancellablePromise);
cancellablePromise.finally(() => this.installingExtensions.delete(key));
}
return cancellablePromise;
}
private async doInstallFromGallery(extension: IGalleryExtension, options: InstallOptions, token: CancellationToken): Promise<ILocalExtension> {
const startTime = new Date().getTime();
let operation: InstallOperation = InstallOperation.Install;
this.logService.info('Installing extension:', extension.identifier.id);
this._onInstallExtension.fire({ identifier: extension.identifier, gallery: extension });
try {
const installed = await this.getInstalled(ExtensionType.User);
const existingExtension = installed.find(i => areSameExtensions(i.identifier, extension.identifier));
if (existingExtension) {
operation = InstallOperation.Update;
}
const installableExtension = await this.downloadInstallableExtension(extension, operation);
installableExtension.metadata.isMachineScoped = options.isMachineScoped || existingExtension?.isMachineScoped;
installableExtension.metadata.isBuiltin = options.isBuiltin || existingExtension?.isBuiltin;
const local = await this.installExtension(installableExtension, token);
try { await this.extensionsDownloader.delete(URI.file(installableExtension.zipPath)); } catch (error) { /* Ignore */ }
try {
await this.installDependenciesAndPackExtensions(local, existingExtension, options);
} catch (error) {
try { await this.uninstall(local); } catch (error) { /* Ignore */ }
throw error;
}
if (existingExtension && semver.neq(existingExtension.manifest.version, extension.version)) {
await this.setUninstalled(existingExtension);
}
this.logService.info(`Extensions installed successfully:`, extension.identifier.id);
this._onDidInstallExtension.fire({ identifier: extension.identifier, gallery: extension, local, operation });
this.reportTelemetry(this.getTelemetryEvent(operation), getGalleryExtensionTelemetryData(extension), new Date().getTime() - startTime, undefined);
return local;
} catch (error) {
const errorCode = error && (<ExtensionManagementError>error).code ? (<ExtensionManagementError>error).code : ERROR_UNKNOWN;
this.logService.error(`Failed to install extension:`, extension.identifier.id, error ? error.message : errorCode);
this._onDidInstallExtension.fire({ identifier: extension.identifier, gallery: extension, operation, error: errorCode });
this.reportTelemetry(this.getTelemetryEvent(operation), getGalleryExtensionTelemetryData(extension), new Date().getTime() - startTime, error);
if (error instanceof Error) {
error.name = errorCode;
}
throw error;
}
}
private async checkAndGetCompatibleVersion(extension: IGalleryExtension): Promise<IGalleryExtension> {
if (await this.isMalicious(extension)) {
throw new ExtensionManagementError(nls.localize('malicious extension', "Can't install extension since it was reported to be problematic."), INSTALL_ERROR_MALICIOUS);
}
const compatibleExtension = await this.galleryService.getCompatibleExtension(extension);
if (!compatibleExtension) {
throw new ExtensionManagementError(nls.localize('notFoundCompatibleDependency', "Unable to install '{0}' extension because it is not compatible with the current version of VS Code (version {1}).", extension.identifier.id, product.version), INSTALL_ERROR_INCOMPATIBLE);
}
return compatibleExtension;
}
async reinstallFromGallery(extension: ILocalExtension): Promise<void> {
this.logService.trace('ExtensionManagementService#reinstallFromGallery', extension.identifier.id);
if (!this.galleryService.isEnabled()) {
throw new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"));
}
const galleryExtension = await this.findGalleryExtension(extension);
if (!galleryExtension) {
throw new Error(nls.localize('Not a Marketplace extension', "Only Marketplace Extensions can be reinstalled"));
}
await this.setUninstalled(extension);
try {
await this.extensionsScanner.removeUninstalledExtension(extension);
} catch (e) {
throw new Error(nls.localize('removeError', "Error while removing the extension: {0}. Please Quit and Start VS Code before trying again.", toErrorMessage(e)));
}
await this.installFromGallery(galleryExtension);
}
private getTelemetryEvent(operation: InstallOperation): string {
return operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install';
}
private async isMalicious(extension: IGalleryExtension): Promise<boolean> {
const report = await this.getExtensionsReport();
return getMaliciousExtensionsSet(report).has(extension.identifier.id);
}
private async downloadInstallableExtension(extension: IGalleryExtension, operation: InstallOperation): Promise<Required<InstallableExtension>> {
const metadata = <IGalleryMetadata>{
id: extension.identifier.uuid,
publisherId: extension.publisherId,
publisherDisplayName: extension.publisherDisplayName,
};
let zipPath;
try {
this.logService.trace('Started downloading extension:', extension.identifier.id);
const zip = await this.extensionsDownloader.downloadExtension(extension, operation);
this.logService.info('Downloaded extension:', extension.identifier.id, zipPath);
zipPath = zip.fsPath;
} catch (error) {
throw new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_DOWNLOADING);
}
try {
const manifest = await getManifest(zipPath);
return (<Required<InstallableExtension>>{ zipPath, identifierWithVersion: new ExtensionIdentifierWithVersion(extension.identifier, manifest.version), metadata });
} catch (error) {
throw new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_VALIDATING);
}
}
private async installExtension(installableExtension: InstallableExtension, token: CancellationToken): Promise<ILocalExtension> {
try {
const local = await this.unsetUninstalledAndGetLocal(installableExtension.identifierWithVersion);
if (local) {
return installableExtension.metadata ? this.extensionsScanner.saveMetadataForLocalExtension(local, installableExtension.metadata) : local;
}
} catch (e) {
if (isMacintosh) {
throw new ExtensionManagementError(nls.localize('quitCode', "Unable to install the extension. Please Quit and Start VS Code before reinstalling."), INSTALL_ERROR_UNSET_UNINSTALLED);
} else {
throw new ExtensionManagementError(nls.localize('exitCode', "Unable to install the extension. Please Exit and Start VS Code before reinstalling."), INSTALL_ERROR_UNSET_UNINSTALLED);
}
}
return this.extractAndInstall(installableExtension, token);
}
private async unsetUninstalledAndGetLocal(identifierWithVersion: ExtensionIdentifierWithVersion): Promise<ILocalExtension | null> {
const isUninstalled = await this.isUninstalled(identifierWithVersion);
if (!isUninstalled) {
return null;
}
this.logService.trace('Removing the extension from uninstalled list:', identifierWithVersion.id);
// If the same version of extension is marked as uninstalled, remove it from there and return the local.
await this.unsetUninstalled(identifierWithVersion);
this.logService.info('Removed the extension from uninstalled list:', identifierWithVersion.id);
const installed = await this.getInstalled(ExtensionType.User);
return installed.find(i => new ExtensionIdentifierWithVersion(i.identifier, i.manifest.version).equals(identifierWithVersion)) || null;
}
private async extractAndInstall({ zipPath, identifierWithVersion, metadata }: InstallableExtension, token: CancellationToken): Promise<ILocalExtension> {
let local = await this.extensionsScanner.extractUserExtension(identifierWithVersion, zipPath, token);
this.logService.info('Installation completed.', identifierWithVersion.id);
if (metadata) {
local = await this.extensionsScanner.saveMetadataForLocalExtension(local, metadata);
}
return local;
}
private async installDependenciesAndPackExtensions(installed: ILocalExtension, existing: ILocalExtension | undefined, options: InstallOptions): Promise<void> {
if (!this.galleryService.isEnabled()) {
return;
}
const dependenciesAndPackExtensions: string[] = installed.manifest.extensionDependencies || [];
if (installed.manifest.extensionPack) {
for (const extension of installed.manifest.extensionPack) {
// add only those extensions which are new in currently installed extension
if (!(existing && existing.manifest.extensionPack && existing.manifest.extensionPack.some(old => areSameExtensions({ id: old }, { id: extension })))) {
if (dependenciesAndPackExtensions.every(e => !areSameExtensions({ id: e }, { id: extension }))) {
dependenciesAndPackExtensions.push(extension);
}
}
}
}
if (dependenciesAndPackExtensions.length) {
const installed = await this.getInstalled();
// filter out installed extensions
const names = dependenciesAndPackExtensions.filter(id => installed.every(({ identifier: galleryIdentifier }) => !areSameExtensions(galleryIdentifier, { id })));
if (names.length) {
const galleryResult = await this.galleryService.query({ names, pageSize: dependenciesAndPackExtensions.length }, CancellationToken.None);
const extensionsToInstall = galleryResult.firstPage;
try {
await Promise.all(extensionsToInstall.map(e => this.installFromGallery(e, options)));
} catch (error) {
try { await this.rollback(extensionsToInstall); } catch (e) { /* ignore */ }
throw error;
}
}
}
}
private async rollback(extensions: IGalleryExtension[]): Promise<void> {
const installed = await this.getInstalled(ExtensionType.User);
const extensionsToUninstall = installed.filter(local => extensions.some(galleryExtension => new ExtensionIdentifierWithVersion(local.identifier, local.manifest.version).equals(new ExtensionIdentifierWithVersion(galleryExtension.identifier, galleryExtension.version)))); // Check with version because we want to rollback the exact version
await Promise.all(extensionsToUninstall.map(local => this.uninstall(local)));
}
async uninstall(extension: ILocalExtension): Promise<void> {
this.logService.trace('ExtensionManagementService#uninstall', extension.identifier.id);
const installed = await this.getInstalled(ExtensionType.User);
const extensionToUninstall = installed.find(e => areSameExtensions(e.identifier, extension.identifier));
if (!extensionToUninstall) {
throw new Error(nls.localize('notInstalled', "Extension '{0}' is not installed.", extension.manifest.displayName || extension.manifest.name));
}
try {
await this.checkForDependenciesAndUninstall(extensionToUninstall, installed);
} catch (error) {
throw this.joinErrors(error);
}
}
async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension> {
this.logService.trace('ExtensionManagementService#updateMetadata', local.identifier.id);
local = await this.extensionsScanner.saveMetadataForLocalExtension(local, { ...((<ILocalExtensionManifest>local.manifest).__metadata || {}), ...metadata });
this.manifestCache.invalidate();
return local;
}
async updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise<ILocalExtension> {
this.logService.trace('ExtensionManagementService#updateExtensionScope', local.identifier.id);
local = await this.extensionsScanner.saveMetadataForLocalExtension(local, { ...((<ILocalExtensionManifest>local.manifest).__metadata || {}), isMachineScoped });
this.manifestCache.invalidate();
return local;
}
private async getGalleryMetadata(extensionName: string): Promise<IGalleryMetadata | undefined> {
const galleryExtension = await this.findGalleryExtensionByName(extensionName);
return galleryExtension ? <IGalleryMetadata>{ id: galleryExtension.identifier.uuid, publisherDisplayName: galleryExtension.publisherDisplayName, publisherId: galleryExtension.publisherId } : undefined;
}
private async findGalleryExtension(local: ILocalExtension): Promise<IGalleryExtension> {
if (local.identifier.uuid) {
const galleryExtension = await this.findGalleryExtensionById(local.identifier.uuid);
return galleryExtension ? galleryExtension : this.findGalleryExtensionByName(local.identifier.id);
}
return this.findGalleryExtensionByName(local.identifier.id);
}
private async findGalleryExtensionById(uuid: string): Promise<IGalleryExtension> {
const galleryResult = await this.galleryService.query({ ids: [uuid], pageSize: 1 }, CancellationToken.None);
return galleryResult.firstPage[0];
}
private async findGalleryExtensionByName(name: string): Promise<IGalleryExtension> {
const galleryResult = await this.galleryService.query({ names: [name], pageSize: 1 }, CancellationToken.None);
return galleryResult.firstPage[0];
}
private joinErrors(errorOrErrors: (Error | string) | (Array<Error | string>)): Error {
const errors = Array.isArray(errorOrErrors) ? errorOrErrors : [errorOrErrors];
if (errors.length === 1) {
return errors[0] instanceof Error ? <Error>errors[0] : new Error(<string>errors[0]);
}
return errors.reduce<Error>((previousValue: Error, currentValue: Error | string) => {
return new Error(`${previousValue.message}${previousValue.message ? ',' : ''}${currentValue instanceof Error ? currentValue.message : currentValue}`);
}, new Error(''));
}
private async checkForDependenciesAndUninstall(extension: ILocalExtension, installed: ILocalExtension[]): Promise<void> {
try {
await this.preUninstallExtension(extension);
const packedExtensions = this.getAllPackExtensionsToUninstall(extension, installed);
if (packedExtensions.length) {
await this.uninstallExtensions(extension, packedExtensions, installed);
} else {
await this.uninstallExtensions(extension, [], installed);
}
} catch (error) {
await this.postUninstallExtension(extension, new ExtensionManagementError(error instanceof Error ? error.message : error, INSTALL_ERROR_LOCAL));
throw error;
}
await this.postUninstallExtension(extension);
}
private async uninstallExtensions(extension: ILocalExtension, otherExtensionsToUninstall: ILocalExtension[], installed: ILocalExtension[]): Promise<void> {
const extensionsToUninstall = [extension, ...otherExtensionsToUninstall];
for (const e of extensionsToUninstall) {
this.checkForDependents(e, extensionsToUninstall, installed, extension);
}
await Promise.all([this.uninstallExtension(extension), ...otherExtensionsToUninstall.map(d => this.doUninstall(d))]);
}
private checkForDependents(extension: ILocalExtension, extensionsToUninstall: ILocalExtension[], installed: ILocalExtension[], extensionToUninstall: ILocalExtension): void {
const dependents = this.getDependents(extension, installed);
if (dependents.length) {
const remainingDependents = dependents.filter(dependent => extensionsToUninstall.indexOf(dependent) === -1);
if (remainingDependents.length) {
throw new Error(this.getDependentsErrorMessage(extension, remainingDependents, extensionToUninstall));
}
}
}
private getDependentsErrorMessage(dependingExtension: ILocalExtension, dependents: ILocalExtension[], extensionToUninstall: ILocalExtension): string {
if (extensionToUninstall === dependingExtension) {
if (dependents.length === 1) {
return nls.localize('singleDependentError', "Cannot uninstall '{0}' extension. '{1}' extension depends on this.",
extensionToUninstall.manifest.displayName || extensionToUninstall.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name);
}
if (dependents.length === 2) {
return nls.localize('twoDependentsError', "Cannot uninstall '{0}' extension. '{1}' and '{2}' extensions depend on this.",
extensionToUninstall.manifest.displayName || extensionToUninstall.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name);
}
return nls.localize('multipleDependentsError', "Cannot uninstall '{0}' extension. '{1}', '{2}' and other extension depend on this.",
extensionToUninstall.manifest.displayName || extensionToUninstall.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name);
}
if (dependents.length === 1) {
return nls.localize('singleIndirectDependentError', "Cannot uninstall '{0}' extension . It includes uninstalling '{1}' extension and '{2}' extension depends on this.",
extensionToUninstall.manifest.displayName || extensionToUninstall.manifest.name, dependingExtension.manifest.displayName
|| dependingExtension.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name);
}
if (dependents.length === 2) {
return nls.localize('twoIndirectDependentsError', "Cannot uninstall '{0}' extension. It includes uninstalling '{1}' extension and '{2}' and '{3}' extensions depend on this.",
extensionToUninstall.manifest.displayName || extensionToUninstall.manifest.name, dependingExtension.manifest.displayName
|| dependingExtension.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name);
}
return nls.localize('multipleIndirectDependentsError', "Cannot uninstall '{0}' extension. It includes uninstalling '{1}' extension and '{2}', '{3}' and other extensions depend on this.",
extensionToUninstall.manifest.displayName || extensionToUninstall.manifest.name, dependingExtension.manifest.displayName
|| dependingExtension.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name);
}
private getAllPackExtensionsToUninstall(extension: ILocalExtension, installed: ILocalExtension[], checked: ILocalExtension[] = []): ILocalExtension[] {
if (checked.indexOf(extension) !== -1) {
return [];
}
checked.push(extension);
const extensionsPack = extension.manifest.extensionPack ? extension.manifest.extensionPack : [];
if (extensionsPack.length) {
const packedExtensions = installed.filter(i => !i.isBuiltin && extensionsPack.some(id => areSameExtensions({ id }, i.identifier)));
const packOfPackedExtensions: ILocalExtension[] = [];
for (const packedExtension of packedExtensions) {
packOfPackedExtensions.push(...this.getAllPackExtensionsToUninstall(packedExtension, installed, checked));
}
return [...packedExtensions, ...packOfPackedExtensions];
}
return [];
}
private getDependents(extension: ILocalExtension, installed: ILocalExtension[]): ILocalExtension[] {
return installed.filter(e => e.manifest.extensionDependencies && e.manifest.extensionDependencies.some(id => areSameExtensions({ id }, extension.identifier)));
}
private async doUninstall(extension: ILocalExtension): Promise<void> {
try {
await this.preUninstallExtension(extension);
await this.uninstallExtension(extension);
} catch (error) {
await this.postUninstallExtension(extension, new ExtensionManagementError(error instanceof Error ? error.message : error, INSTALL_ERROR_LOCAL));
throw error;
}
await this.postUninstallExtension(extension);
}
private async preUninstallExtension(extension: ILocalExtension): Promise<void> {
const exists = await pfs.exists(extension.location.fsPath);
if (!exists) {
throw new Error(nls.localize('notExists', "Could not find extension"));
}
this.logService.info('Uninstalling extension:', extension.identifier.id);
this._onUninstallExtension.fire(extension.identifier);
}
private async uninstallExtension(local: ILocalExtension): Promise<void> {
let promise = this.uninstallingExtensions.get(local.identifier.id);
if (!promise) {
// Set all versions of the extension as uninstalled
promise = createCancelablePromise(async () => {
const userExtensions = await this.extensionsScanner.scanUserExtensions(false);
await this.setUninstalled(...userExtensions.filter(u => areSameExtensions(u.identifier, local.identifier)));
});
this.uninstallingExtensions.set(local.identifier.id, promise);
promise.finally(() => this.uninstallingExtensions.delete(local.identifier.id));
}
return promise;
}
private async postUninstallExtension(extension: ILocalExtension, error?: Error): Promise<void> {
if (error) {
this.logService.error('Failed to uninstall extension:', extension.identifier.id, error.message);
} else {
this.logService.info('Successfully uninstalled extension:', extension.identifier.id);
// only report if extension has a mapped gallery extension. UUID identifies the gallery extension.
if (extension.identifier.uuid) {
try {
await this.galleryService.reportStatistic(extension.manifest.publisher, extension.manifest.name, extension.manifest.version, StatisticType.Uninstall);
} catch (error) { /* ignore */ }
}
}
this.reportTelemetry('extensionGallery:uninstall', getLocalExtensionTelemetryData(extension), undefined, error);
const errorcode = error ? error instanceof ExtensionManagementError ? error.code : ERROR_UNKNOWN : undefined;
this._onDidUninstallExtension.fire({ identifier: extension.identifier, error: errorcode });
}
getInstalled(type: ExtensionType | null = null): Promise<ILocalExtension[]> {
return this.extensionsScanner.scanExtensions(type);
}
removeDeprecatedExtensions(): Promise<void> {
return this.extensionsScanner.cleanUp();
}
private async isUninstalled(identifier: ExtensionIdentifierWithVersion): Promise<boolean> {
const uninstalled = await this.filterUninstalled(identifier);
return uninstalled.length === 1;
}
private filterUninstalled(...identifiers: ExtensionIdentifierWithVersion[]): Promise<string[]> {
return this.extensionsScanner.withUninstalledExtensions(allUninstalled => {
const uninstalled: string[] = [];
for (const identifier of identifiers) {
if (!!allUninstalled[identifier.key()]) {
uninstalled.push(identifier.key());
}
}
return uninstalled;
});
}
private setUninstalled(...extensions: ILocalExtension[]): Promise<{ [id: string]: boolean }> {
const ids: ExtensionIdentifierWithVersion[] = extensions.map(e => new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version));
return this.extensionsScanner.withUninstalledExtensions(uninstalled => {
ids.forEach(id => uninstalled[id.key()] = true);
return uninstalled;
});
}
private unsetUninstalled(extensionIdentifier: ExtensionIdentifierWithVersion): Promise<void> {
return this.extensionsScanner.withUninstalledExtensions<void>(uninstalled => delete uninstalled[extensionIdentifier.key()]);
}
getExtensionsReport(): Promise<IReportedExtension[]> {
const now = new Date().getTime();
if (!this.reportedExtensions || now - this.lastReportTimestamp > 1000 * 60 * 5) { // 5 minute cache freshness
this.reportedExtensions = this.updateReportCache();
this.lastReportTimestamp = now;
}
return this.reportedExtensions;
}
private async updateReportCache(): Promise<IReportedExtension[]> {
try {
this.logService.trace('ExtensionManagementService.refreshReportedCache');
const result = await this.galleryService.getExtensionsReport();
this.logService.trace(`ExtensionManagementService.refreshReportedCache - got ${result.length} reported extensions from service`);
return result;
} catch (err) {
this.logService.trace('ExtensionManagementService.refreshReportedCache - failed to get extension report');
return [];
}
}
private reportTelemetry(eventName: string, extensionData: any, duration?: number, error?: Error): void {
const errorcode = error ? error instanceof ExtensionManagementError ? error.code : ERROR_UNKNOWN : undefined;
/* __GDPR__
"extensionGallery:install" : {
"success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
"recommendationReason": { "retiredFromVersion": "1.23.0", "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"${include}": [
"${GalleryExtensionTelemetryData}"
]
}
*/
/* __GDPR__
"extensionGallery:uninstall" : {
"success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
"${include}": [
"${GalleryExtensionTelemetryData}"
]
}
*/
/* __GDPR__
"extensionGallery:update" : {
"success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
"${include}": [
"${GalleryExtensionTelemetryData}"
]
}
*/
this.telemetryService.publicLogError(eventName, { ...extensionData, success: !error, duration, errorcode });
}
}

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { buffer } from 'vs/base/node/zip';
import { localize } from 'vs/nls';
import { IExtensionManifest } from 'vs/platform/extensions/common/extensions';
export function getManifest(vsix: string): Promise<IExtensionManifest> {
return buffer(vsix, 'extension/package.json')
.then(buffer => {
try {
return JSON.parse(buffer.toString('utf8'));
} catch (err) {
throw new Error(localize('invalidManifest', "VSIX invalid: package.json is not a JSON file."));
}
});
}

View File

@@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { join } from 'vs/base/common/path';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { IExtensionManagementService, DidInstallExtensionEvent, DidUninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement';
import { MANIFEST_CACHE_FOLDER, USER_MANIFEST_CACHE_FILE } from 'vs/platform/extensions/common/extensions';
import * as pfs from 'vs/base/node/pfs';
export class ExtensionsManifestCache extends Disposable {
private extensionsManifestCache = join(this.environmentService.userDataPath, MANIFEST_CACHE_FOLDER, USER_MANIFEST_CACHE_FILE);
constructor(
private readonly environmentService: INativeEnvironmentService,
extensionsManagementService: IExtensionManagementService
) {
super();
this._register(extensionsManagementService.onDidInstallExtension(e => this.onDidInstallExtension(e)));
this._register(extensionsManagementService.onDidUninstallExtension(e => this.onDidUnInstallExtension(e)));
}
private onDidInstallExtension(e: DidInstallExtensionEvent): void {
if (!e.error) {
this.invalidate();
}
}
private onDidUnInstallExtension(e: DidUninstallExtensionEvent): void {
if (!e.error) {
this.invalidate();
}
}
invalidate(): void {
pfs.rimraf(this.extensionsManifestCache, pfs.RimRafMode.MOVE).then(() => { }, () => { });
}
}

View File

@@ -0,0 +1,373 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as semver from 'vs/base/common/semver/semver';
import { Disposable } from 'vs/base/common/lifecycle';
import * as pfs from 'vs/base/node/pfs';
import * as path from 'vs/base/common/path';
import { ILogService } from 'vs/platform/log/common/log';
import { ILocalExtension, IGalleryMetadata, ExtensionManagementError } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionType, IExtensionManifest, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { areSameExtensions, ExtensionIdentifierWithVersion, groupByExtension, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { Limiter, Queue } from 'vs/base/common/async';
import { URI } from 'vs/base/common/uri';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls';
import { localize } from 'vs/nls';
import { IProductService } from 'vs/platform/product/common/productService';
import { CancellationToken } from 'vscode';
import { extract, ExtractError } from 'vs/base/node/zip';
import { isWindows } from 'vs/base/common/platform';
import { flatten } from 'vs/base/common/arrays';
import { IStringDictionary } from 'vs/base/common/collections';
import { FileAccess } from 'vs/base/common/network';
const ERROR_SCANNING_SYS_EXTENSIONS = 'scanningSystem';
const ERROR_SCANNING_USER_EXTENSIONS = 'scanningUser';
const INSTALL_ERROR_EXTRACTING = 'extracting';
const INSTALL_ERROR_DELETING = 'deleting';
const INSTALL_ERROR_RENAMING = 'renaming';
export type IMetadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; isBuiltin: boolean }>;
export type ILocalExtensionManifest = IExtensionManifest & { __metadata?: IMetadata };
type IRelaxedLocalExtension = Omit<ILocalExtension, 'isBuiltin'> & { isBuiltin: boolean };
export class ExtensionsScanner extends Disposable {
private readonly systemExtensionsPath: string;
private readonly extensionsPath: string;
private readonly uninstalledPath: string;
private readonly uninstalledFileLimiter: Queue<any>;
constructor(
private readonly beforeRemovingExtension: (e: ILocalExtension) => Promise<void>,
@ILogService private readonly logService: ILogService,
@INativeEnvironmentService private readonly environmentService: INativeEnvironmentService,
@IProductService private readonly productService: IProductService,
) {
super();
this.systemExtensionsPath = environmentService.builtinExtensionsPath;
this.extensionsPath = environmentService.extensionsPath!;
this.uninstalledPath = path.join(this.extensionsPath, '.obsolete');
this.uninstalledFileLimiter = new Queue();
}
async cleanUp(): Promise<void> {
await this.removeUninstalledExtensions();
await this.removeOutdatedExtensions();
}
async scanExtensions(type: ExtensionType | null): Promise<ILocalExtension[]> {
const promises: Promise<ILocalExtension[]>[] = [];
if (type === null || type === ExtensionType.System) {
promises.push(this.scanSystemExtensions().then(null, e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, ERROR_SCANNING_SYS_EXTENSIONS))));
}
if (type === null || type === ExtensionType.User) {
promises.push(this.scanUserExtensions(true).then(null, e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, ERROR_SCANNING_USER_EXTENSIONS))));
}
try {
const result = await Promise.all(promises);
return flatten(result);
} catch (error) {
throw this.joinErrors(error);
}
}
async scanUserExtensions(excludeOutdated: boolean): Promise<ILocalExtension[]> {
this.logService.trace('Started scanning user extensions');
let [uninstalled, extensions] = await Promise.all([this.getUninstalledExtensions(), this.scanAllUserExtensions()]);
extensions = extensions.filter(e => !uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]);
if (excludeOutdated) {
const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier);
extensions = byExtension.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]);
}
this.logService.trace('Scanned user extensions:', extensions.length);
return extensions;
}
async scanAllUserExtensions(): Promise<ILocalExtension[]> {
return this.scanExtensionsInDir(this.extensionsPath, ExtensionType.User);
}
async extractUserExtension(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, token: CancellationToken): Promise<ILocalExtension> {
const folderName = identifierWithVersion.key();
const tempPath = path.join(this.extensionsPath, `.${folderName}`);
const extensionPath = path.join(this.extensionsPath, folderName);
try {
await pfs.rimraf(extensionPath);
} catch (error) {
try {
await pfs.rimraf(extensionPath);
} catch (e) { /* ignore */ }
throw new ExtensionManagementError(localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionPath, identifierWithVersion.id), INSTALL_ERROR_DELETING);
}
await this.extractAtLocation(identifierWithVersion, zipPath, tempPath, token);
try {
await this.rename(identifierWithVersion, tempPath, extensionPath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */);
this.logService.info('Renamed to', extensionPath);
} catch (error) {
this.logService.info('Rename failed. Deleting from extracted location', tempPath);
try {
pfs.rimraf(tempPath);
} catch (e) { /* ignore */ }
throw error;
}
let local: ILocalExtension | null = null;
try {
local = await this.scanExtension(folderName, this.extensionsPath, ExtensionType.User);
} catch (e) { /*ignore */ }
if (local) {
return local;
}
throw new Error(localize('cannot read', "Cannot read the extension from {0}", this.extensionsPath));
}
async saveMetadataForLocalExtension(local: ILocalExtension, metadata: IMetadata): Promise<ILocalExtension> {
this.setMetadata(local, metadata);
// unset if false
metadata.isMachineScoped = metadata.isMachineScoped || undefined;
metadata.isBuiltin = metadata.isBuiltin || undefined;
const manifestPath = path.join(local.location.fsPath, 'package.json');
const raw = await pfs.readFile(manifestPath, 'utf8');
const { manifest } = await this.parseManifest(raw);
(manifest as ILocalExtensionManifest).__metadata = metadata;
await pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t'));
return local;
}
getUninstalledExtensions(): Promise<{ [id: string]: boolean; }> {
return this.withUninstalledExtensions(uninstalled => uninstalled);
}
async withUninstalledExtensions<T>(fn: (uninstalled: IStringDictionary<boolean>) => T): Promise<T> {
return this.uninstalledFileLimiter.queue(async () => {
let raw: string | undefined;
try {
raw = await pfs.readFile(this.uninstalledPath, 'utf8');
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
}
let uninstalled = {};
if (raw) {
try {
uninstalled = JSON.parse(raw);
} catch (e) { /* ignore */ }
}
const result = fn(uninstalled);
if (Object.keys(uninstalled).length) {
await pfs.writeFile(this.uninstalledPath, JSON.stringify(uninstalled));
} else {
await pfs.rimraf(this.uninstalledPath);
}
return result;
});
}
async removeExtension(extension: ILocalExtension, type: string): Promise<void> {
this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id, extension.location.fsPath);
await pfs.rimraf(extension.location.fsPath);
this.logService.info('Deleted from disk', extension.identifier.id, extension.location.fsPath);
}
async removeUninstalledExtension(extension: ILocalExtension): Promise<void> {
await this.removeExtension(extension, 'uninstalled');
await this.withUninstalledExtensions(uninstalled => delete uninstalled[new ExtensionIdentifierWithVersion(extension.identifier, extension.manifest.version).key()]);
}
private async extractAtLocation(identifier: IExtensionIdentifier, zipPath: string, location: string, token: CancellationToken): Promise<void> {
this.logService.trace(`Started extracting the extension from ${zipPath} to ${location}`);
// Clean the location
try {
await pfs.rimraf(location);
} catch (e) {
throw new ExtensionManagementError(this.joinErrors(e).message, INSTALL_ERROR_DELETING);
}
try {
await extract(zipPath, location, { sourcePath: 'extension', overwrite: true }, token);
this.logService.info(`Extracted extension to ${location}:`, identifier.id);
} catch (e) {
try { await pfs.rimraf(location); } catch (e) { /* Ignore */ }
throw new ExtensionManagementError(e.message, e instanceof ExtractError && e.type ? e.type : INSTALL_ERROR_EXTRACTING);
}
}
private async rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise<void> {
try {
await pfs.rename(extractPath, renamePath);
} catch (error) {
if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) {
this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identifier.id);
return this.rename(identifier, extractPath, renamePath, retryUntil);
}
throw new ExtensionManagementError(error.message || localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || INSTALL_ERROR_RENAMING);
}
}
private async scanSystemExtensions(): Promise<ILocalExtension[]> {
this.logService.trace('Started scanning system extensions');
const systemExtensionsPromise = this.scanDefaultSystemExtensions();
if (this.environmentService.isBuilt) {
return systemExtensionsPromise;
}
// Scan other system extensions during development
const devSystemExtensionsPromise = this.scanDevSystemExtensions();
const [systemExtensions, devSystemExtensions] = await Promise.all([systemExtensionsPromise, devSystemExtensionsPromise]);
return [...systemExtensions, ...devSystemExtensions];
}
private async scanExtensionsInDir(dir: string, type: ExtensionType): Promise<ILocalExtension[]> {
const limiter = new Limiter<any>(10);
const extensionsFolders = await pfs.readdir(dir);
const extensions = await Promise.all<ILocalExtension>(extensionsFolders.map(extensionFolder => limiter.queue(() => this.scanExtension(extensionFolder, dir, type))));
return extensions.filter(e => e && e.identifier);
}
private async scanExtension(folderName: string, root: string, type: ExtensionType): Promise<ILocalExtension | null> {
if (type === ExtensionType.User && folderName.indexOf('.') === 0) { // Do not consider user extension folder starting with `.`
return null;
}
const extensionPath = path.join(root, folderName);
try {
const children = await pfs.readdir(extensionPath);
const { manifest, metadata } = await this.readManifest(extensionPath);
const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0];
const readmeUrl = readme ? URI.file(path.join(extensionPath, readme)) : undefined;
const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0];
const changelogUrl = changelog ? URI.file(path.join(extensionPath, changelog)) : undefined;
const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };
const local = <ILocalExtension>{ type, identifier, manifest, location: URI.file(extensionPath), readmeUrl, changelogUrl, publisherDisplayName: null, publisherId: null, isMachineScoped: false, isBuiltin: type === ExtensionType.System };
if (metadata) {
this.setMetadata(local, metadata);
}
return local;
} catch (e) {
this.logService.trace(e);
return null;
}
}
private async scanDefaultSystemExtensions(): Promise<ILocalExtension[]> {
const result = await this.scanExtensionsInDir(this.systemExtensionsPath, ExtensionType.System);
this.logService.trace('Scanned system extensions:', result.length);
return result;
}
private async scanDevSystemExtensions(): Promise<ILocalExtension[]> {
const devSystemExtensionsList = this.getDevSystemExtensionsList();
if (devSystemExtensionsList.length) {
const result = await this.scanExtensionsInDir(this.devSystemExtensionsPath, ExtensionType.System);
this.logService.trace('Scanned dev system extensions:', result.length);
return result.filter(r => devSystemExtensionsList.some(id => areSameExtensions(r.identifier, { id })));
} else {
return [];
}
}
private setMetadata(local: IRelaxedLocalExtension, metadata: IMetadata): void {
local.publisherDisplayName = metadata.publisherDisplayName || null;
local.publisherId = metadata.publisherId || null;
local.identifier.uuid = metadata.id;
local.isMachineScoped = !!metadata.isMachineScoped;
local.isBuiltin = local.type === ExtensionType.System || !!metadata.isBuiltin;
}
private async removeUninstalledExtensions(): Promise<void> {
const uninstalled = await this.getUninstalledExtensions();
const extensions = await this.scanAllUserExtensions(); // All user extensions
const installed: Set<string> = new Set<string>();
for (const e of extensions) {
if (!uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]) {
installed.add(e.identifier.id.toLowerCase());
}
}
const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier);
await Promise.all(byExtension.map(async e => {
const latest = e.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0];
if (!installed.has(latest.identifier.id.toLowerCase())) {
await this.beforeRemovingExtension(latest);
}
}));
const toRemove: ILocalExtension[] = extensions.filter(e => uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]);
await Promise.all(toRemove.map(e => this.removeUninstalledExtension(e)));
}
private async removeOutdatedExtensions(): Promise<void> {
const extensions = await this.scanAllUserExtensions();
const toRemove: ILocalExtension[] = [];
// Outdated extensions
const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier);
toRemove.push(...flatten(byExtension.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version)).slice(1))));
await Promise.all(toRemove.map(extension => this.removeExtension(extension, 'outdated')));
}
private getDevSystemExtensionsList(): string[] {
return (this.productService.builtInExtensions || []).map(e => e.name);
}
private joinErrors(errorOrErrors: (Error | string) | (Array<Error | string>)): Error {
const errors = Array.isArray(errorOrErrors) ? errorOrErrors : [errorOrErrors];
if (errors.length === 1) {
return errors[0] instanceof Error ? <Error>errors[0] : new Error(<string>errors[0]);
}
return errors.reduce<Error>((previousValue: Error, currentValue: Error | string) => {
return new Error(`${previousValue.message}${previousValue.message ? ',' : ''}${currentValue instanceof Error ? currentValue.message : currentValue}`);
}, new Error(''));
}
private _devSystemExtensionsPath: string | null = null;
private get devSystemExtensionsPath(): string {
if (!this._devSystemExtensionsPath) {
this._devSystemExtensionsPath = path.normalize(path.join(FileAccess.asFileUri('', require).fsPath, '..', '.build', 'builtInExtensions'));
}
return this._devSystemExtensionsPath;
}
private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IMetadata | null; }> {
const promises = [
pfs.readFile(path.join(extensionPath, 'package.json'), 'utf8')
.then(raw => this.parseManifest(raw)),
pfs.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8')
.then(undefined, err => err.code !== 'ENOENT' ? Promise.reject<string>(err) : '{}')
.then(raw => JSON.parse(raw))
];
const [{ manifest, metadata }, translations] = await Promise.all(promises);
return {
manifest: localizeManifest(manifest, translations),
metadata
};
}
private parseManifest(raw: string): Promise<{ manifest: IExtensionManifest; metadata: IMetadata | null; }> {
return new Promise((c, e) => {
try {
const manifest = JSON.parse(raw);
const metadata = manifest.__metadata || null;
c({ manifest, metadata });
} catch (err) {
e(new Error(localize('invalidManifest', "Extension invalid: package.json is not a JSON file.")));
}
});
}
}

View File

@@ -0,0 +1,130 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { getDomainsOfRemotes, getRemotes } from 'vs/platform/extensionManagement/common/configRemotes';
suite('Config Remotes', () => {
const allowedDomains = [
'github.com',
'github2.com',
'github3.com',
'example.com',
'example2.com',
'example3.com',
'server.org',
'server2.org',
];
test('HTTPS remotes', function () {
assert.deepStrictEqual(getDomainsOfRemotes(remote('https://github.com/microsoft/vscode.git'), allowedDomains), ['github.com']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('https://git.example.com/gitproject.git'), allowedDomains), ['example.com']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('https://username@github2.com/username/repository.git'), allowedDomains), ['github2.com']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('https://username:password@github3.com/username/repository.git'), allowedDomains), ['github3.com']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('https://username:password@example2.com:1234/username/repository.git'), allowedDomains), ['example2.com']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('https://example3.com:1234/username/repository.git'), allowedDomains), ['example3.com']);
});
test('SSH remotes', function () {
assert.deepStrictEqual(getDomainsOfRemotes(remote('ssh://user@git.server.org/project.git'), allowedDomains), ['server.org']);
});
test('SCP-like remotes', function () {
assert.deepStrictEqual(getDomainsOfRemotes(remote('git@github.com:microsoft/vscode.git'), allowedDomains), ['github.com']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('user@git.server.org:project.git'), allowedDomains), ['server.org']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('git.server2.org:project.git'), allowedDomains), ['server2.org']);
});
test('Local remotes', function () {
assert.deepStrictEqual(getDomainsOfRemotes(remote('/opt/git/project.git'), allowedDomains), []);
assert.deepStrictEqual(getDomainsOfRemotes(remote('file:///opt/git/project.git'), allowedDomains), []);
});
test('Multiple remotes', function () {
const config = ['https://github.com/microsoft/vscode.git', 'https://git.example.com/gitproject.git'].map(remote).join('');
assert.deepStrictEqual(getDomainsOfRemotes(config, allowedDomains).sort(), ['example.com', 'github.com']);
});
test('Non allowed domains are anonymized', () => {
const config = ['https://github.com/microsoft/vscode.git', 'https://git.foobar.com/gitproject.git'].map(remote).join('');
assert.deepStrictEqual(getDomainsOfRemotes(config, allowedDomains).sort(), ['aaaaaa.aaa', 'github.com']);
});
test('HTTPS remotes to be hashed', function () {
assert.deepStrictEqual(getRemotes(remote('https://github.com/microsoft/vscode.git')), ['github.com/microsoft/vscode.git']);
assert.deepStrictEqual(getRemotes(remote('https://git.example.com/gitproject.git')), ['git.example.com/gitproject.git']);
assert.deepStrictEqual(getRemotes(remote('https://username@github2.com/username/repository.git')), ['github2.com/username/repository.git']);
assert.deepStrictEqual(getRemotes(remote('https://username:password@github3.com/username/repository.git')), ['github3.com/username/repository.git']);
assert.deepStrictEqual(getRemotes(remote('https://username:password@example2.com:1234/username/repository.git')), ['example2.com/username/repository.git']);
assert.deepStrictEqual(getRemotes(remote('https://example3.com:1234/username/repository.git')), ['example3.com/username/repository.git']);
// Strip .git
assert.deepStrictEqual(getRemotes(remote('https://github.com/microsoft/vscode.git'), true), ['github.com/microsoft/vscode']);
assert.deepStrictEqual(getRemotes(remote('https://git.example.com/gitproject.git'), true), ['git.example.com/gitproject']);
assert.deepStrictEqual(getRemotes(remote('https://username@github2.com/username/repository.git'), true), ['github2.com/username/repository']);
assert.deepStrictEqual(getRemotes(remote('https://username:password@github3.com/username/repository.git'), true), ['github3.com/username/repository']);
assert.deepStrictEqual(getRemotes(remote('https://username:password@example2.com:1234/username/repository.git'), true), ['example2.com/username/repository']);
assert.deepStrictEqual(getRemotes(remote('https://example3.com:1234/username/repository.git'), true), ['example3.com/username/repository']);
// Compare Striped .git with no .git
assert.deepStrictEqual(getRemotes(remote('https://github.com/microsoft/vscode.git'), true), getRemotes(remote('https://github.com/microsoft/vscode')));
assert.deepStrictEqual(getRemotes(remote('https://git.example.com/gitproject.git'), true), getRemotes(remote('https://git.example.com/gitproject')));
assert.deepStrictEqual(getRemotes(remote('https://username@github2.com/username/repository.git'), true), getRemotes(remote('https://username@github2.com/username/repository')));
assert.deepStrictEqual(getRemotes(remote('https://username:password@github3.com/username/repository.git'), true), getRemotes(remote('https://username:password@github3.com/username/repository')));
assert.deepStrictEqual(getRemotes(remote('https://username:password@example2.com:1234/username/repository.git'), true), getRemotes(remote('https://username:password@example2.com:1234/username/repository')));
assert.deepStrictEqual(getRemotes(remote('https://example3.com:1234/username/repository.git'), true), getRemotes(remote('https://example3.com:1234/username/repository')));
});
test('SSH remotes to be hashed', function () {
assert.deepStrictEqual(getRemotes(remote('ssh://user@git.server.org/project.git')), ['git.server.org/project.git']);
// Strip .git
assert.deepStrictEqual(getRemotes(remote('ssh://user@git.server.org/project.git'), true), ['git.server.org/project']);
// Compare Striped .git with no .git
assert.deepStrictEqual(getRemotes(remote('ssh://user@git.server.org/project.git'), true), getRemotes(remote('ssh://user@git.server.org/project')));
});
test('SCP-like remotes to be hashed', function () {
assert.deepStrictEqual(getRemotes(remote('git@github.com:microsoft/vscode.git')), ['github.com/microsoft/vscode.git']);
assert.deepStrictEqual(getRemotes(remote('user@git.server.org:project.git')), ['git.server.org/project.git']);
assert.deepStrictEqual(getRemotes(remote('git.server2.org:project.git')), ['git.server2.org/project.git']);
// Strip .git
assert.deepStrictEqual(getRemotes(remote('git@github.com:microsoft/vscode.git'), true), ['github.com/microsoft/vscode']);
assert.deepStrictEqual(getRemotes(remote('user@git.server.org:project.git'), true), ['git.server.org/project']);
assert.deepStrictEqual(getRemotes(remote('git.server2.org:project.git'), true), ['git.server2.org/project']);
// Compare Striped .git with no .git
assert.deepStrictEqual(getRemotes(remote('git@github.com:microsoft/vscode.git'), true), getRemotes(remote('git@github.com:microsoft/vscode')));
assert.deepStrictEqual(getRemotes(remote('user@git.server.org:project.git'), true), getRemotes(remote('user@git.server.org:project')));
assert.deepStrictEqual(getRemotes(remote('git.server2.org:project.git'), true), getRemotes(remote('git.server2.org:project')));
});
test('Local remotes to be hashed', function () {
assert.deepStrictEqual(getRemotes(remote('/opt/git/project.git')), []);
assert.deepStrictEqual(getRemotes(remote('file:///opt/git/project.git')), []);
});
test('Multiple remotes to be hashed', function () {
const config = ['https://github.com/microsoft/vscode.git', 'https://git.example.com/gitproject.git'].map(remote).join(' ');
assert.deepStrictEqual(getRemotes(config), ['github.com/microsoft/vscode.git', 'git.example.com/gitproject.git']);
// Strip .git
assert.deepStrictEqual(getRemotes(config, true), ['github.com/microsoft/vscode', 'git.example.com/gitproject']);
// Compare Striped .git with no .git
const noDotGitConfig = ['https://github.com/microsoft/vscode', 'https://git.example.com/gitproject'].map(remote).join(' ');
assert.deepStrictEqual(getRemotes(config, true), getRemotes(noDotGitConfig));
});
function remote(url: string): string {
return `[remote "origin"]
url = ${url}
fetch = +refs/heads/*:refs/remotes/origin/*
`;
}
});

View File

@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/common/extensionManagement';
suite('Extension Identifier Pattern', () => {
test('extension identifier pattern', () => {
const regEx = new RegExp(EXTENSION_IDENTIFIER_PATTERN);
assert.equal(true, regEx.test('publisher.name'));
assert.equal(true, regEx.test('publiSher.name'));
assert.equal(true, regEx.test('publisher.Name'));
assert.equal(true, regEx.test('PUBLISHER.NAME'));
assert.equal(true, regEx.test('PUBLISHEr.NAMe'));
assert.equal(true, regEx.test('PUBLISHEr.N-AMe'));
assert.equal(true, regEx.test('PUB-LISHEr.NAMe'));
assert.equal(true, regEx.test('PUB-LISHEr.N-AMe'));
assert.equal(true, regEx.test('PUBLISH12Er90.N-A54Me123'));
assert.equal(true, regEx.test('111PUBLISH12Er90.N-1111A54Me123'));
assert.equal(false, regEx.test('publishername'));
assert.equal(false, regEx.test('-publisher.name'));
assert.equal(false, regEx.test('publisher.-name'));
assert.equal(false, regEx.test('-publisher.-name'));
assert.equal(false, regEx.test('publ_isher.name'));
assert.equal(false, regEx.test('publisher._name'));
});
});

View File

@@ -0,0 +1,67 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as os from 'os';
import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { join } from 'vs/base/common/path';
import { mkdirp, RimRafMode, rimraf } from 'vs/base/node/pfs';
import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/common/extensionGalleryService';
import { isUUID } from 'vs/base/common/uuid';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IFileService } from 'vs/platform/files/common/files';
import { FileService } from 'vs/platform/files/common/fileService';
import { NullLogService } from 'vs/platform/log/common/log';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { Schemas } from 'vs/base/common/network';
import product from 'vs/platform/product/common/product';
import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
import { IStorageService } from 'vs/platform/storage/common/storage';
suite('Extension Gallery Service', () => {
const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'extensiongalleryservice');
const marketplaceHome = join(parentDir, 'Marketplace');
let fileService: IFileService;
let disposables: DisposableStore;
setup(done => {
disposables = new DisposableStore();
fileService = new FileService(new NullLogService());
disposables.add(fileService);
const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService());
disposables.add(diskFileSystemProvider);
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
// Delete any existing backups completely and then re-create it.
rimraf(marketplaceHome, RimRafMode.MOVE).then(() => {
mkdirp(marketplaceHome).then(() => {
done();
}, error => done(error));
}, error => done(error));
});
teardown(done => {
disposables.clear();
rimraf(marketplaceHome, RimRafMode.MOVE).then(done, done);
});
test('marketplace machine id', () => {
const args = ['--user-data-dir', marketplaceHome];
const environmentService = new NativeEnvironmentService(parseArgs(args, OPTIONS));
const storageService: IStorageService = new TestStorageService();
return resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService).then(headers => {
assert.ok(isUUID(headers['X-Market-User-Id']));
return resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService).then(headers2 => {
assert.equal(headers['X-Market-User-Id'], headers2['X-Market-User-Id']);
});
});
});
});

View File

@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const enum RecommendationSource {
FILE = 1,
WORKSPACE = 2,
EXE = 3
}
export const enum RecommendationsNotificationResult {
Ignored = 'ignored',
Cancelled = 'cancelled',
TooMany = 'toomany',
Accepted = 'reacted',
}
export const IExtensionRecommendationNotificationService = createDecorator<IExtensionRecommendationNotificationService>('IExtensionRecommendationNotificationService');
export interface IExtensionRecommendationNotificationService {
readonly _serviceBrand: undefined;
readonly ignoredRecommendations: string[];
hasToIgnoreRecommendationNotifications(): boolean;
promptImportantExtensionsInstallNotification(extensionIds: string[], message: string, searchValue: string, source: RecommendationSource): Promise<RecommendationsNotificationResult>;
promptWorkspaceRecommendations(recommendations: string[]): Promise<void>;
}

View File

@@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
export class ExtensionRecommendationNotificationServiceChannelClient implements IExtensionRecommendationNotificationService {
declare readonly _serviceBrand: undefined;
constructor(private readonly channel: IChannel) { }
get ignoredRecommendations(): string[] { throw new Error('not supported'); }
promptImportantExtensionsInstallNotification(extensionIds: string[], message: string, searchValue: string, priority: RecommendationSource): Promise<RecommendationsNotificationResult> {
return this.channel.call('promptImportantExtensionsInstallNotification', [extensionIds, message, searchValue, priority]);
}
promptWorkspaceRecommendations(recommendations: string[]): Promise<void> {
throw new Error('not supported');
}
hasToIgnoreRecommendationNotifications(): boolean {
throw new Error('not supported');
}
}
export class ExtensionRecommendationNotificationServiceChannel implements IServerChannel {
constructor(private service: IExtensionRecommendationNotificationService) { }
listen(_: unknown, event: string): Event<any> {
throw new Error(`Event not found: ${event}`);
}
call(_: unknown, command: string, args?: any): Promise<any> {
switch (command) {
case 'promptImportantExtensionsInstallNotification': return this.service.promptImportantExtensionsInstallNotification(args[0], args[1], args[2], args[3]);
}
throw new Error(`Call not found: ${command}`);
}
}

View File

@@ -0,0 +1,260 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
export interface IParsedVersion {
hasCaret: boolean;
hasGreaterEquals: boolean;
majorBase: number;
majorMustEqual: boolean;
minorBase: number;
minorMustEqual: boolean;
patchBase: number;
patchMustEqual: boolean;
preRelease: string | null;
}
export interface INormalizedVersion {
majorBase: number;
majorMustEqual: boolean;
minorBase: number;
minorMustEqual: boolean;
patchBase: number;
patchMustEqual: boolean;
isMinimum: boolean;
}
const VERSION_REGEXP = /^(\^|>=)?((\d+)|x)\.((\d+)|x)\.((\d+)|x)(\-.*)?$/;
export function isValidVersionStr(version: string): boolean {
version = version.trim();
return (version === '*' || VERSION_REGEXP.test(version));
}
export function parseVersion(version: string): IParsedVersion | null {
if (!isValidVersionStr(version)) {
return null;
}
version = version.trim();
if (version === '*') {
return {
hasCaret: false,
hasGreaterEquals: false,
majorBase: 0,
majorMustEqual: false,
minorBase: 0,
minorMustEqual: false,
patchBase: 0,
patchMustEqual: false,
preRelease: null
};
}
let m = version.match(VERSION_REGEXP);
if (!m) {
return null;
}
return {
hasCaret: m[1] === '^',
hasGreaterEquals: m[1] === '>=',
majorBase: m[2] === 'x' ? 0 : parseInt(m[2], 10),
majorMustEqual: (m[2] === 'x' ? false : true),
minorBase: m[4] === 'x' ? 0 : parseInt(m[4], 10),
minorMustEqual: (m[4] === 'x' ? false : true),
patchBase: m[6] === 'x' ? 0 : parseInt(m[6], 10),
patchMustEqual: (m[6] === 'x' ? false : true),
preRelease: m[8] || null
};
}
export function normalizeVersion(version: IParsedVersion | null): INormalizedVersion | null {
if (!version) {
return null;
}
let majorBase = version.majorBase,
majorMustEqual = version.majorMustEqual,
minorBase = version.minorBase,
minorMustEqual = version.minorMustEqual,
patchBase = version.patchBase,
patchMustEqual = version.patchMustEqual;
if (version.hasCaret) {
if (majorBase === 0) {
patchMustEqual = false;
} else {
minorMustEqual = false;
patchMustEqual = false;
}
}
return {
majorBase: majorBase,
majorMustEqual: majorMustEqual,
minorBase: minorBase,
minorMustEqual: minorMustEqual,
patchBase: patchBase,
patchMustEqual: patchMustEqual,
isMinimum: version.hasGreaterEquals
};
}
export function isValidVersion(_version: string | INormalizedVersion, _desiredVersion: string | INormalizedVersion): boolean {
let version: INormalizedVersion | null;
if (typeof _version === 'string') {
version = normalizeVersion(parseVersion(_version));
} else {
version = _version;
}
let desiredVersion: INormalizedVersion | null;
if (typeof _desiredVersion === 'string') {
desiredVersion = normalizeVersion(parseVersion(_desiredVersion));
} else {
desiredVersion = _desiredVersion;
}
if (!version || !desiredVersion) {
return false;
}
let majorBase = version.majorBase;
let minorBase = version.minorBase;
let patchBase = version.patchBase;
let desiredMajorBase = desiredVersion.majorBase;
let desiredMinorBase = desiredVersion.minorBase;
let desiredPatchBase = desiredVersion.patchBase;
let majorMustEqual = desiredVersion.majorMustEqual;
let minorMustEqual = desiredVersion.minorMustEqual;
let patchMustEqual = desiredVersion.patchMustEqual;
if (desiredVersion.isMinimum) {
if (majorBase > desiredMajorBase) {
return true;
}
if (majorBase < desiredMajorBase) {
return false;
}
if (minorBase > desiredMinorBase) {
return true;
}
if (minorBase < desiredMinorBase) {
return false;
}
return patchBase >= desiredPatchBase;
}
// Anything < 1.0.0 is compatible with >= 1.0.0, except exact matches
if (majorBase === 1 && desiredMajorBase === 0 && (!majorMustEqual || !minorMustEqual || !patchMustEqual)) {
desiredMajorBase = 1;
desiredMinorBase = 0;
desiredPatchBase = 0;
majorMustEqual = true;
minorMustEqual = false;
patchMustEqual = false;
}
if (majorBase < desiredMajorBase) {
// smaller major version
return false;
}
if (majorBase > desiredMajorBase) {
// higher major version
return (!majorMustEqual);
}
// at this point, majorBase are equal
if (minorBase < desiredMinorBase) {
// smaller minor version
return false;
}
if (minorBase > desiredMinorBase) {
// higher minor version
return (!minorMustEqual);
}
// at this point, minorBase are equal
if (patchBase < desiredPatchBase) {
// smaller patch version
return false;
}
if (patchBase > desiredPatchBase) {
// higher patch version
return (!patchMustEqual);
}
// at this point, patchBase are equal
return true;
}
export interface IReducedExtensionDescription {
isBuiltin: boolean;
engines: {
vscode: string;
};
main?: string;
}
export function isValidExtensionVersion(version: string, extensionDesc: IReducedExtensionDescription, notices: string[]): boolean {
if (extensionDesc.isBuiltin || typeof extensionDesc.main === 'undefined') {
// No version check for builtin or declarative extensions
return true;
}
return isVersionValid(version, extensionDesc.engines.vscode, notices);
}
export function isEngineValid(engine: string, version: string): boolean {
// TODO@joao: discuss with alex '*' doesn't seem to be a valid engine version
return engine === '*' || isVersionValid(version, engine);
}
export function isVersionValid(currentVersion: string, requestedVersion: string, notices: string[] = []): boolean {
let desiredVersion = normalizeVersion(parseVersion(requestedVersion));
if (!desiredVersion) {
notices.push(nls.localize('versionSyntax', "Could not parse `engines.vscode` value {0}. Please use, for example: ^1.22.0, ^1.22.x, etc.", requestedVersion));
return false;
}
// enforce that a breaking API version is specified.
// for 0.X.Y, that means up to 0.X must be specified
// otherwise for Z.X.Y, that means Z must be specified
if (desiredVersion.majorBase === 0) {
// force that major and minor must be specific
if (!desiredVersion.majorMustEqual || !desiredVersion.minorMustEqual) {
notices.push(nls.localize('versionSpecificity1', "Version specified in `engines.vscode` ({0}) is not specific enough. For vscode versions before 1.0.0, please define at a minimum the major and minor desired version. E.g. ^0.10.0, 0.10.x, 0.11.0, etc.", requestedVersion));
return false;
}
} else {
// force that major must be specific
if (!desiredVersion.majorMustEqual) {
notices.push(nls.localize('versionSpecificity2', "Version specified in `engines.vscode` ({0}) is not specific enough. For vscode versions after 1.0.0, please define at a minimum the major desired version. E.g. ^1.10.0, 1.10.x, 1.x.x, 2.x.x, etc.", requestedVersion));
return false;
}
}
if (!isValidVersion(currentVersion, desiredVersion)) {
notices.push(nls.localize('versionMismatch', "Extension is not compatible with Code {0}. Extension requires: {1}.", currentVersion, requestedVersion));
return false;
}
return true;
}

View File

@@ -0,0 +1,307 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as strings from 'vs/base/common/strings';
import { ILocalization } from 'vs/platform/localizations/common/localizations';
import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const MANIFEST_CACHE_FOLDER = 'CachedExtensions';
export const USER_MANIFEST_CACHE_FILE = 'user';
export const BUILTIN_MANIFEST_CACHE_FILE = 'builtin';
export interface ICommand {
command: string;
title: string;
category?: string;
}
export interface IConfigurationProperty {
description: string;
type: string | string[];
default?: any;
}
export interface IConfiguration {
properties: { [key: string]: IConfigurationProperty; };
}
export interface IDebugger {
label?: string;
type: string;
runtime?: string;
}
export interface IGrammar {
language: string;
}
export interface IJSONValidation {
fileMatch: string | string[];
url: string;
}
export interface IKeyBinding {
command: string;
key: string;
when?: string;
mac?: string;
linux?: string;
win?: string;
}
export interface ILanguage {
id: string;
extensions: string[];
aliases: string[];
}
export interface IMenu {
command: string;
alt?: string;
when?: string;
group?: string;
}
export interface ISnippet {
language: string;
}
export interface ITheme {
label: string;
}
export interface IViewContainer {
id: string;
title: string;
}
export interface IView {
id: string;
name: string;
}
export interface IColor {
id: string;
description: string;
defaults: { light: string, dark: string, highContrast: string };
}
export interface IWebviewEditor {
readonly viewType: string;
readonly priority: string;
readonly selector: readonly {
readonly filenamePattern?: string;
}[];
}
export interface ICodeActionContributionAction {
readonly kind: string;
readonly title: string;
readonly description?: string;
}
export interface ICodeActionContribution {
readonly languages: readonly string[];
readonly actions: readonly ICodeActionContributionAction[];
}
export interface IAuthenticationContribution {
readonly id: string;
readonly label: string;
}
export interface IExtensionContributions {
commands?: ICommand[];
configuration?: IConfiguration | IConfiguration[];
debuggers?: IDebugger[];
grammars?: IGrammar[];
jsonValidation?: IJSONValidation[];
keybindings?: IKeyBinding[];
languages?: ILanguage[];
menus?: { [context: string]: IMenu[] };
snippets?: ISnippet[];
themes?: ITheme[];
iconThemes?: ITheme[];
viewsContainers?: { [location: string]: IViewContainer[] };
views?: { [location: string]: IView[] };
colors?: IColor[];
localizations?: ILocalization[];
readonly customEditors?: readonly IWebviewEditor[];
readonly codeActions?: readonly ICodeActionContribution[];
authentication?: IAuthenticationContribution[];
}
export type ExtensionKind = 'ui' | 'workspace' | 'web';
export function isIExtensionIdentifier(thing: any): thing is IExtensionIdentifier {
return thing
&& typeof thing === 'object'
&& typeof thing.id === 'string'
&& (!thing.uuid || typeof thing.uuid === 'string');
}
export interface IExtensionIdentifier {
id: string;
uuid?: string;
}
export const EXTENSION_CATEGORIES = [
'Azure',
'Data Science',
'Debuggers',
'Extension Packs',
'Formatters',
'Keymaps',
'Language Packs',
'Linters',
'Machine Learning',
'Notebooks',
'Programming Languages',
'SCM Providers',
'Snippets',
'Themes',
'Testing',
'Visualization',
'Other',
];
export interface IExtensionManifest {
readonly name: string;
readonly displayName?: string;
readonly publisher: string;
readonly version: string;
readonly engines: { vscode: string };
readonly description?: string;
readonly main?: string;
readonly browser?: string;
readonly icon?: string;
readonly categories?: string[];
readonly keywords?: string[];
readonly activationEvents?: string[];
readonly extensionDependencies?: string[];
readonly extensionPack?: string[];
readonly extensionKind?: ExtensionKind | ExtensionKind[];
readonly contributes?: IExtensionContributions;
readonly repository?: { url: string; };
readonly bugs?: { url: string; };
readonly enableProposedApi?: boolean;
readonly api?: string;
readonly scripts?: { [key: string]: string; };
}
export const enum ExtensionType {
System,
User
}
export interface IExtension {
readonly type: ExtensionType;
readonly isBuiltin: boolean;
readonly identifier: IExtensionIdentifier;
readonly manifest: IExtensionManifest;
readonly location: URI;
readonly readmeUrl?: URI;
readonly changelogUrl?: URI;
}
/**
* **!Do not construct directly!**
*
* **!Only static methods because it gets serialized!**
*
* This represents the "canonical" version for an extension identifier. Extension ids
* have to be case-insensitive (due to the marketplace), but we must ensure case
* preservation because the extension API is already public at this time.
*
* For example, given an extension with the publisher `"Hello"` and the name `"World"`,
* its canonical extension identifier is `"Hello.World"`. This extension could be
* referenced in some other extension's dependencies using the string `"hello.world"`.
*
* To make matters more complicated, an extension can optionally have an UUID. When two
* extensions have the same UUID, they are considered equal even if their identifier is different.
*/
export class ExtensionIdentifier {
public readonly value: string;
private readonly _lower: string;
constructor(value: string) {
this.value = value;
this._lower = value.toLowerCase();
}
public static equals(a: ExtensionIdentifier | string | null | undefined, b: ExtensionIdentifier | string | null | undefined) {
if (typeof a === 'undefined' || a === null) {
return (typeof b === 'undefined' || b === null);
}
if (typeof b === 'undefined' || b === null) {
return false;
}
if (typeof a === 'string' || typeof b === 'string') {
// At least one of the arguments is an extension id in string form,
// so we have to use the string comparison which ignores case.
let aValue = (typeof a === 'string' ? a : a.value);
let bValue = (typeof b === 'string' ? b : b.value);
return strings.equalsIgnoreCase(aValue, bValue);
}
// Now we know both arguments are ExtensionIdentifier
return (a._lower === b._lower);
}
/**
* Gives the value by which to index (for equality).
*/
public static toKey(id: ExtensionIdentifier | string): string {
if (typeof id === 'string') {
return id.toLowerCase();
}
return id._lower;
}
}
export interface IExtensionDescription extends IExtensionManifest {
readonly identifier: ExtensionIdentifier;
readonly uuid?: string;
readonly isBuiltin: boolean;
readonly isUserBuiltin: boolean;
readonly isUnderDevelopment: boolean;
readonly extensionLocation: URI;
enableProposedApi?: boolean;
}
export function isLanguagePackExtension(manifest: IExtensionManifest): boolean {
return manifest.contributes && manifest.contributes.localizations ? manifest.contributes.localizations.length > 0 : false;
}
export function isAuthenticaionProviderExtension(manifest: IExtensionManifest): boolean {
return manifest.contributes && manifest.contributes.authentication ? manifest.contributes.authentication.length > 0 : false;
}
export interface IScannedExtension {
readonly identifier: IExtensionIdentifier;
readonly location: URI;
readonly type: ExtensionType;
readonly packageJSON: IExtensionManifest;
readonly packageNLS?: any;
readonly packageNLSUrl?: URI;
readonly readmeUrl?: URI;
readonly changelogUrl?: URI;
}
export interface ITranslatedScannedExtension {
readonly identifier: IExtensionIdentifier;
readonly location: URI;
readonly type: ExtensionType;
readonly packageJSON: IExtensionManifest;
readonly readmeUrl?: URI;
readonly changelogUrl?: URI;
}
export const IBuiltinExtensionsScannerService = createDecorator<IBuiltinExtensionsScannerService>('IBuiltinExtensionsScannerService');
export interface IBuiltinExtensionsScannerService {
readonly _serviceBrand: undefined;
scanBuiltinExtensions(): Promise<IScannedExtension[]>;
}

View File

@@ -0,0 +1,394 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { INormalizedVersion, IParsedVersion, IReducedExtensionDescription, isValidExtensionVersion, isValidVersion, isValidVersionStr, normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator';
suite('Extension Version Validator', () => {
test('isValidVersionStr', () => {
assert.equal(isValidVersionStr('0.10.0-dev'), true);
assert.equal(isValidVersionStr('0.10.0'), true);
assert.equal(isValidVersionStr('0.10.1'), true);
assert.equal(isValidVersionStr('0.10.100'), true);
assert.equal(isValidVersionStr('0.11.0'), true);
assert.equal(isValidVersionStr('x.x.x'), true);
assert.equal(isValidVersionStr('0.x.x'), true);
assert.equal(isValidVersionStr('0.10.0'), true);
assert.equal(isValidVersionStr('0.10.x'), true);
assert.equal(isValidVersionStr('^0.10.0'), true);
assert.equal(isValidVersionStr('*'), true);
assert.equal(isValidVersionStr('0.x.x.x'), false);
assert.equal(isValidVersionStr('0.10'), false);
assert.equal(isValidVersionStr('0.10.'), false);
});
test('parseVersion', () => {
function assertParseVersion(version: string, hasCaret: boolean, hasGreaterEquals: boolean, majorBase: number, majorMustEqual: boolean, minorBase: number, minorMustEqual: boolean, patchBase: number, patchMustEqual: boolean, preRelease: string | null): void {
const actual = parseVersion(version);
const expected: IParsedVersion = { hasCaret, hasGreaterEquals, majorBase, majorMustEqual, minorBase, minorMustEqual, patchBase, patchMustEqual, preRelease };
assert.deepEqual(actual, expected, 'parseVersion for ' + version);
}
assertParseVersion('0.10.0-dev', false, false, 0, true, 10, true, 0, true, '-dev');
assertParseVersion('0.10.0', false, false, 0, true, 10, true, 0, true, null);
assertParseVersion('0.10.1', false, false, 0, true, 10, true, 1, true, null);
assertParseVersion('0.10.100', false, false, 0, true, 10, true, 100, true, null);
assertParseVersion('0.11.0', false, false, 0, true, 11, true, 0, true, null);
assertParseVersion('x.x.x', false, false, 0, false, 0, false, 0, false, null);
assertParseVersion('0.x.x', false, false, 0, true, 0, false, 0, false, null);
assertParseVersion('0.10.x', false, false, 0, true, 10, true, 0, false, null);
assertParseVersion('^0.10.0', true, false, 0, true, 10, true, 0, true, null);
assertParseVersion('^0.10.2', true, false, 0, true, 10, true, 2, true, null);
assertParseVersion('^1.10.2', true, false, 1, true, 10, true, 2, true, null);
assertParseVersion('*', false, false, 0, false, 0, false, 0, false, null);
assertParseVersion('>=0.0.1', false, true, 0, true, 0, true, 1, true, null);
assertParseVersion('>=2.4.3', false, true, 2, true, 4, true, 3, true, null);
});
test('normalizeVersion', () => {
function assertNormalizeVersion(version: string, majorBase: number, majorMustEqual: boolean, minorBase: number, minorMustEqual: boolean, patchBase: number, patchMustEqual: boolean, isMinimum: boolean): void {
const actual = normalizeVersion(parseVersion(version));
const expected: INormalizedVersion = { majorBase, majorMustEqual, minorBase, minorMustEqual, patchBase, patchMustEqual, isMinimum };
assert.deepEqual(actual, expected, 'parseVersion for ' + version);
}
assertNormalizeVersion('0.10.0-dev', 0, true, 10, true, 0, true, false);
assertNormalizeVersion('0.10.0', 0, true, 10, true, 0, true, false);
assertNormalizeVersion('0.10.1', 0, true, 10, true, 1, true, false);
assertNormalizeVersion('0.10.100', 0, true, 10, true, 100, true, false);
assertNormalizeVersion('0.11.0', 0, true, 11, true, 0, true, false);
assertNormalizeVersion('x.x.x', 0, false, 0, false, 0, false, false);
assertNormalizeVersion('0.x.x', 0, true, 0, false, 0, false, false);
assertNormalizeVersion('0.10.x', 0, true, 10, true, 0, false, false);
assertNormalizeVersion('^0.10.0', 0, true, 10, true, 0, false, false);
assertNormalizeVersion('^0.10.2', 0, true, 10, true, 2, false, false);
assertNormalizeVersion('^1.10.2', 1, true, 10, false, 2, false, false);
assertNormalizeVersion('*', 0, false, 0, false, 0, false, false);
assertNormalizeVersion('>=0.0.1', 0, true, 0, true, 1, true, true);
assertNormalizeVersion('>=2.4.3', 2, true, 4, true, 3, true, true);
});
test('isValidVersion', () => {
function testIsValidVersion(version: string, desiredVersion: string, expectedResult: boolean): void {
let actual = isValidVersion(version, desiredVersion);
assert.equal(actual, expectedResult, 'extension - vscode: ' + version + ', desiredVersion: ' + desiredVersion + ' should be ' + expectedResult);
}
testIsValidVersion('0.10.0-dev', 'x.x.x', true);
testIsValidVersion('0.10.0-dev', '0.x.x', true);
testIsValidVersion('0.10.0-dev', '0.10.0', true);
testIsValidVersion('0.10.0-dev', '0.10.2', false);
testIsValidVersion('0.10.0-dev', '^0.10.2', false);
testIsValidVersion('0.10.0-dev', '0.10.x', true);
testIsValidVersion('0.10.0-dev', '^0.10.0', true);
testIsValidVersion('0.10.0-dev', '*', true);
testIsValidVersion('0.10.0-dev', '>=0.0.1', true);
testIsValidVersion('0.10.0-dev', '>=0.0.10', true);
testIsValidVersion('0.10.0-dev', '>=0.10.0', true);
testIsValidVersion('0.10.0-dev', '>=0.10.1', false);
testIsValidVersion('0.10.0-dev', '>=1.0.0', false);
testIsValidVersion('0.10.0', 'x.x.x', true);
testIsValidVersion('0.10.0', '0.x.x', true);
testIsValidVersion('0.10.0', '0.10.0', true);
testIsValidVersion('0.10.0', '0.10.2', false);
testIsValidVersion('0.10.0', '^0.10.2', false);
testIsValidVersion('0.10.0', '0.10.x', true);
testIsValidVersion('0.10.0', '^0.10.0', true);
testIsValidVersion('0.10.0', '*', true);
testIsValidVersion('0.10.1', 'x.x.x', true);
testIsValidVersion('0.10.1', '0.x.x', true);
testIsValidVersion('0.10.1', '0.10.0', false);
testIsValidVersion('0.10.1', '0.10.2', false);
testIsValidVersion('0.10.1', '^0.10.2', false);
testIsValidVersion('0.10.1', '0.10.x', true);
testIsValidVersion('0.10.1', '^0.10.0', true);
testIsValidVersion('0.10.1', '*', true);
testIsValidVersion('0.10.100', 'x.x.x', true);
testIsValidVersion('0.10.100', '0.x.x', true);
testIsValidVersion('0.10.100', '0.10.0', false);
testIsValidVersion('0.10.100', '0.10.2', false);
testIsValidVersion('0.10.100', '^0.10.2', true);
testIsValidVersion('0.10.100', '0.10.x', true);
testIsValidVersion('0.10.100', '^0.10.0', true);
testIsValidVersion('0.10.100', '*', true);
testIsValidVersion('0.11.0', 'x.x.x', true);
testIsValidVersion('0.11.0', '0.x.x', true);
testIsValidVersion('0.11.0', '0.10.0', false);
testIsValidVersion('0.11.0', '0.10.2', false);
testIsValidVersion('0.11.0', '^0.10.2', false);
testIsValidVersion('0.11.0', '0.10.x', false);
testIsValidVersion('0.11.0', '^0.10.0', false);
testIsValidVersion('0.11.0', '*', true);
// Anything < 1.0.0 is compatible
testIsValidVersion('1.0.0', 'x.x.x', true);
testIsValidVersion('1.0.0', '0.x.x', true);
testIsValidVersion('1.0.0', '0.10.0', false);
testIsValidVersion('1.0.0', '0.10.2', false);
testIsValidVersion('1.0.0', '^0.10.2', true);
testIsValidVersion('1.0.0', '0.10.x', true);
testIsValidVersion('1.0.0', '^0.10.0', true);
testIsValidVersion('1.0.0', '1.0.0', true);
testIsValidVersion('1.0.0', '^1.0.0', true);
testIsValidVersion('1.0.0', '^2.0.0', false);
testIsValidVersion('1.0.0', '*', true);
testIsValidVersion('1.0.0', '>=0.0.1', true);
testIsValidVersion('1.0.0', '>=0.0.10', true);
testIsValidVersion('1.0.0', '>=0.10.0', true);
testIsValidVersion('1.0.0', '>=0.10.1', true);
testIsValidVersion('1.0.0', '>=1.0.0', true);
testIsValidVersion('1.0.0', '>=1.1.0', false);
testIsValidVersion('1.0.0', '>=1.0.1', false);
testIsValidVersion('1.0.0', '>=2.0.0', false);
testIsValidVersion('1.0.100', 'x.x.x', true);
testIsValidVersion('1.0.100', '0.x.x', true);
testIsValidVersion('1.0.100', '0.10.0', false);
testIsValidVersion('1.0.100', '0.10.2', false);
testIsValidVersion('1.0.100', '^0.10.2', true);
testIsValidVersion('1.0.100', '0.10.x', true);
testIsValidVersion('1.0.100', '^0.10.0', true);
testIsValidVersion('1.0.100', '1.0.0', false);
testIsValidVersion('1.0.100', '^1.0.0', true);
testIsValidVersion('1.0.100', '^1.0.1', true);
testIsValidVersion('1.0.100', '^2.0.0', false);
testIsValidVersion('1.0.100', '*', true);
testIsValidVersion('1.100.0', 'x.x.x', true);
testIsValidVersion('1.100.0', '0.x.x', true);
testIsValidVersion('1.100.0', '0.10.0', false);
testIsValidVersion('1.100.0', '0.10.2', false);
testIsValidVersion('1.100.0', '^0.10.2', true);
testIsValidVersion('1.100.0', '0.10.x', true);
testIsValidVersion('1.100.0', '^0.10.0', true);
testIsValidVersion('1.100.0', '1.0.0', false);
testIsValidVersion('1.100.0', '^1.0.0', true);
testIsValidVersion('1.100.0', '^1.1.0', true);
testIsValidVersion('1.100.0', '^1.100.0', true);
testIsValidVersion('1.100.0', '^2.0.0', false);
testIsValidVersion('1.100.0', '*', true);
testIsValidVersion('1.100.0', '>=1.99.0', true);
testIsValidVersion('1.100.0', '>=1.100.0', true);
testIsValidVersion('1.100.0', '>=1.101.0', false);
testIsValidVersion('2.0.0', 'x.x.x', true);
testIsValidVersion('2.0.0', '0.x.x', false);
testIsValidVersion('2.0.0', '0.10.0', false);
testIsValidVersion('2.0.0', '0.10.2', false);
testIsValidVersion('2.0.0', '^0.10.2', false);
testIsValidVersion('2.0.0', '0.10.x', false);
testIsValidVersion('2.0.0', '^0.10.0', false);
testIsValidVersion('2.0.0', '1.0.0', false);
testIsValidVersion('2.0.0', '^1.0.0', false);
testIsValidVersion('2.0.0', '^1.1.0', false);
testIsValidVersion('2.0.0', '^1.100.0', false);
testIsValidVersion('2.0.0', '^2.0.0', true);
testIsValidVersion('2.0.0', '*', true);
});
test('isValidExtensionVersion', () => {
function testExtensionVersion(version: string, desiredVersion: string, isBuiltin: boolean, hasMain: boolean, expectedResult: boolean): void {
let desc: IReducedExtensionDescription = {
isBuiltin: isBuiltin,
engines: {
vscode: desiredVersion
},
main: hasMain ? 'something' : undefined
};
let reasons: string[] = [];
let actual = isValidExtensionVersion(version, desc, reasons);
assert.equal(actual, expectedResult, 'version: ' + version + ', desiredVersion: ' + desiredVersion + ', desc: ' + JSON.stringify(desc) + ', reasons: ' + JSON.stringify(reasons));
}
function testIsInvalidExtensionVersion(version: string, desiredVersion: string, isBuiltin: boolean, hasMain: boolean): void {
testExtensionVersion(version, desiredVersion, isBuiltin, hasMain, false);
}
function testIsValidExtensionVersion(version: string, desiredVersion: string, isBuiltin: boolean, hasMain: boolean): void {
testExtensionVersion(version, desiredVersion, isBuiltin, hasMain, true);
}
function testIsValidVersion(version: string, desiredVersion: string, expectedResult: boolean): void {
testExtensionVersion(version, desiredVersion, false, true, expectedResult);
}
// builtin are allowed to use * or x.x.x
testIsValidExtensionVersion('0.10.0-dev', '*', true, true);
testIsValidExtensionVersion('0.10.0-dev', 'x.x.x', true, true);
testIsValidExtensionVersion('0.10.0-dev', '0.x.x', true, true);
testIsValidExtensionVersion('0.10.0-dev', '0.10.x', true, true);
testIsValidExtensionVersion('1.10.0-dev', '1.x.x', true, true);
testIsValidExtensionVersion('1.10.0-dev', '1.10.x', true, true);
testIsValidExtensionVersion('0.10.0-dev', '*', true, false);
testIsValidExtensionVersion('0.10.0-dev', 'x.x.x', true, false);
testIsValidExtensionVersion('0.10.0-dev', '0.x.x', true, false);
testIsValidExtensionVersion('0.10.0-dev', '0.10.x', true, false);
testIsValidExtensionVersion('1.10.0-dev', '1.x.x', true, false);
testIsValidExtensionVersion('1.10.0-dev', '1.10.x', true, false);
// normal extensions are allowed to use * or x.x.x only if they have no main
testIsInvalidExtensionVersion('0.10.0-dev', '*', false, true);
testIsInvalidExtensionVersion('0.10.0-dev', 'x.x.x', false, true);
testIsInvalidExtensionVersion('0.10.0-dev', '0.x.x', false, true);
testIsValidExtensionVersion('0.10.0-dev', '0.10.x', false, true);
testIsValidExtensionVersion('1.10.0-dev', '1.x.x', false, true);
testIsValidExtensionVersion('1.10.0-dev', '1.10.x', false, true);
testIsValidExtensionVersion('0.10.0-dev', '*', false, false);
testIsValidExtensionVersion('0.10.0-dev', 'x.x.x', false, false);
testIsValidExtensionVersion('0.10.0-dev', '0.x.x', false, false);
testIsValidExtensionVersion('0.10.0-dev', '0.10.x', false, false);
testIsValidExtensionVersion('1.10.0-dev', '1.x.x', false, false);
testIsValidExtensionVersion('1.10.0-dev', '1.10.x', false, false);
// extensions without "main" get no version check
testIsValidExtensionVersion('0.10.0-dev', '>=0.9.1-pre.1', false, false);
testIsValidExtensionVersion('0.10.0-dev', '*', false, false);
testIsValidExtensionVersion('0.10.0-dev', 'x.x.x', false, false);
testIsValidExtensionVersion('0.10.0-dev', '0.x.x', false, false);
testIsValidExtensionVersion('0.10.0-dev', '0.10.x', false, false);
testIsValidExtensionVersion('1.10.0-dev', '1.x.x', false, false);
testIsValidExtensionVersion('1.10.0-dev', '1.10.x', false, false);
testIsValidExtensionVersion('0.10.0-dev', '*', false, false);
testIsValidExtensionVersion('0.10.0-dev', 'x.x.x', false, false);
testIsValidExtensionVersion('0.10.0-dev', '0.x.x', false, false);
testIsValidExtensionVersion('0.10.0-dev', '0.10.x', false, false);
testIsValidExtensionVersion('1.10.0-dev', '1.x.x', false, false);
testIsValidExtensionVersion('1.10.0-dev', '1.10.x', false, false);
// normal extensions with code
testIsValidVersion('0.10.0-dev', 'x.x.x', false); // fails due to lack of specificity
testIsValidVersion('0.10.0-dev', '0.x.x', false); // fails due to lack of specificity
testIsValidVersion('0.10.0-dev', '0.10.0', true);
testIsValidVersion('0.10.0-dev', '0.10.2', false);
testIsValidVersion('0.10.0-dev', '^0.10.2', false);
testIsValidVersion('0.10.0-dev', '0.10.x', true);
testIsValidVersion('0.10.0-dev', '^0.10.0', true);
testIsValidVersion('0.10.0-dev', '*', false); // fails due to lack of specificity
testIsValidVersion('0.10.0', 'x.x.x', false); // fails due to lack of specificity
testIsValidVersion('0.10.0', '0.x.x', false); // fails due to lack of specificity
testIsValidVersion('0.10.0', '0.10.0', true);
testIsValidVersion('0.10.0', '0.10.2', false);
testIsValidVersion('0.10.0', '^0.10.2', false);
testIsValidVersion('0.10.0', '0.10.x', true);
testIsValidVersion('0.10.0', '^0.10.0', true);
testIsValidVersion('0.10.0', '*', false); // fails due to lack of specificity
testIsValidVersion('0.10.1', 'x.x.x', false); // fails due to lack of specificity
testIsValidVersion('0.10.1', '0.x.x', false); // fails due to lack of specificity
testIsValidVersion('0.10.1', '0.10.0', false);
testIsValidVersion('0.10.1', '0.10.2', false);
testIsValidVersion('0.10.1', '^0.10.2', false);
testIsValidVersion('0.10.1', '0.10.x', true);
testIsValidVersion('0.10.1', '^0.10.0', true);
testIsValidVersion('0.10.1', '*', false); // fails due to lack of specificity
testIsValidVersion('0.10.100', 'x.x.x', false); // fails due to lack of specificity
testIsValidVersion('0.10.100', '0.x.x', false); // fails due to lack of specificity
testIsValidVersion('0.10.100', '0.10.0', false);
testIsValidVersion('0.10.100', '0.10.2', false);
testIsValidVersion('0.10.100', '^0.10.2', true);
testIsValidVersion('0.10.100', '0.10.x', true);
testIsValidVersion('0.10.100', '^0.10.0', true);
testIsValidVersion('0.10.100', '*', false); // fails due to lack of specificity
testIsValidVersion('0.11.0', 'x.x.x', false); // fails due to lack of specificity
testIsValidVersion('0.11.0', '0.x.x', false); // fails due to lack of specificity
testIsValidVersion('0.11.0', '0.10.0', false);
testIsValidVersion('0.11.0', '0.10.2', false);
testIsValidVersion('0.11.0', '^0.10.2', false);
testIsValidVersion('0.11.0', '0.10.x', false);
testIsValidVersion('0.11.0', '^0.10.0', false);
testIsValidVersion('0.11.0', '*', false); // fails due to lack of specificity
testIsValidVersion('1.0.0', 'x.x.x', false); // fails due to lack of specificity
testIsValidVersion('1.0.0', '0.x.x', false); // fails due to lack of specificity
testIsValidVersion('1.0.0', '0.10.0', false);
testIsValidVersion('1.0.0', '0.10.2', false);
testIsValidVersion('1.0.0', '^0.10.2', true);
testIsValidVersion('1.0.0', '0.10.x', true);
testIsValidVersion('1.0.0', '^0.10.0', true);
testIsValidVersion('1.0.0', '*', false); // fails due to lack of specificity
testIsValidVersion('1.10.0', 'x.x.x', false); // fails due to lack of specificity
testIsValidVersion('1.10.0', '1.x.x', true);
testIsValidVersion('1.10.0', '1.10.0', true);
testIsValidVersion('1.10.0', '1.10.2', false);
testIsValidVersion('1.10.0', '^1.10.2', false);
testIsValidVersion('1.10.0', '1.10.x', true);
testIsValidVersion('1.10.0', '^1.10.0', true);
testIsValidVersion('1.10.0', '*', false); // fails due to lack of specificity
// Anything < 1.0.0 is compatible
testIsValidVersion('1.0.0', 'x.x.x', false); // fails due to lack of specificity
testIsValidVersion('1.0.0', '0.x.x', false); // fails due to lack of specificity
testIsValidVersion('1.0.0', '0.10.0', false);
testIsValidVersion('1.0.0', '0.10.2', false);
testIsValidVersion('1.0.0', '^0.10.2', true);
testIsValidVersion('1.0.0', '0.10.x', true);
testIsValidVersion('1.0.0', '^0.10.0', true);
testIsValidVersion('1.0.0', '1.0.0', true);
testIsValidVersion('1.0.0', '^1.0.0', true);
testIsValidVersion('1.0.0', '^2.0.0', false);
testIsValidVersion('1.0.0', '*', false); // fails due to lack of specificity
testIsValidVersion('1.0.100', 'x.x.x', false); // fails due to lack of specificity
testIsValidVersion('1.0.100', '0.x.x', false); // fails due to lack of specificity
testIsValidVersion('1.0.100', '0.10.0', false);
testIsValidVersion('1.0.100', '0.10.2', false);
testIsValidVersion('1.0.100', '^0.10.2', true);
testIsValidVersion('1.0.100', '0.10.x', true);
testIsValidVersion('1.0.100', '^0.10.0', true);
testIsValidVersion('1.0.100', '1.0.0', false);
testIsValidVersion('1.0.100', '^1.0.0', true);
testIsValidVersion('1.0.100', '^1.0.1', true);
testIsValidVersion('1.0.100', '^2.0.0', false);
testIsValidVersion('1.0.100', '*', false); // fails due to lack of specificity
testIsValidVersion('1.100.0', 'x.x.x', false); // fails due to lack of specificity
testIsValidVersion('1.100.0', '0.x.x', false); // fails due to lack of specificity
testIsValidVersion('1.100.0', '0.10.0', false);
testIsValidVersion('1.100.0', '0.10.2', false);
testIsValidVersion('1.100.0', '^0.10.2', true);
testIsValidVersion('1.100.0', '0.10.x', true);
testIsValidVersion('1.100.0', '^0.10.0', true);
testIsValidVersion('1.100.0', '1.0.0', false);
testIsValidVersion('1.100.0', '^1.0.0', true);
testIsValidVersion('1.100.0', '^1.1.0', true);
testIsValidVersion('1.100.0', '^1.100.0', true);
testIsValidVersion('1.100.0', '^2.0.0', false);
testIsValidVersion('1.100.0', '*', false); // fails due to lack of specificity
testIsValidVersion('2.0.0', 'x.x.x', false); // fails due to lack of specificity
testIsValidVersion('2.0.0', '0.x.x', false); // fails due to lack of specificity
testIsValidVersion('2.0.0', '0.10.0', false);
testIsValidVersion('2.0.0', '0.10.2', false);
testIsValidVersion('2.0.0', '^0.10.2', false);
testIsValidVersion('2.0.0', '0.10.x', false);
testIsValidVersion('2.0.0', '^0.10.0', false);
testIsValidVersion('2.0.0', '1.0.0', false);
testIsValidVersion('2.0.0', '^1.0.0', false);
testIsValidVersion('2.0.0', '^1.1.0', false);
testIsValidVersion('2.0.0', '^1.100.0', false);
testIsValidVersion('2.0.0', '^2.0.0', true);
testIsValidVersion('2.0.0', '*', false); // fails due to lack of specificity
});
});

View File

@@ -0,0 +1,272 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileOverwriteOptions, FileType, FileDeleteOptions, FileWriteOptions, FileChangeType, createFileSystemProviderError, FileSystemProviderErrorCode } from 'vs/platform/files/common/files';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Event, Emitter } from 'vs/base/common/event';
import { VSBuffer } from 'vs/base/common/buffer';
import { joinPath, extUri, dirname } from 'vs/base/common/resources';
import { localize } from 'vs/nls';
import * as browser from 'vs/base/browser/browser';
const INDEXEDDB_VSCODE_DB = 'vscode-web-db';
export const INDEXEDDB_USERDATA_OBJECT_STORE = 'vscode-userdata-store';
export const INDEXEDDB_LOGS_OBJECT_STORE = 'vscode-logs-store';
export class IndexedDB {
private indexedDBPromise: Promise<IDBDatabase | null>;
constructor() {
this.indexedDBPromise = this.openIndexedDB(INDEXEDDB_VSCODE_DB, 2, [INDEXEDDB_USERDATA_OBJECT_STORE, INDEXEDDB_LOGS_OBJECT_STORE]);
}
async createFileSystemProvider(scheme: string, store: string): Promise<IIndexedDBFileSystemProvider | null> {
let fsp: IIndexedDBFileSystemProvider | null = null;
const indexedDB = await this.indexedDBPromise;
if (indexedDB) {
if (indexedDB.objectStoreNames.contains(store)) {
fsp = new IndexedDBFileSystemProvider(scheme, indexedDB, store);
} else {
console.error(`Error while creating indexedDB filesystem provider. Could not find ${store} object store`);
}
}
return fsp;
}
private openIndexedDB(name: string, version: number, stores: string[]): Promise<IDBDatabase | null> {
if (browser.isEdge) {
return Promise.resolve(null);
}
return new Promise((c, e) => {
const request = window.indexedDB.open(name, version);
request.onerror = (err) => e(request.error);
request.onsuccess = () => {
const db = request.result;
for (const store of stores) {
if (!db.objectStoreNames.contains(store)) {
console.error(`Error while creating indexedDB. Could not create ${store} object store`);
c(null);
return;
}
}
c(db);
};
request.onupgradeneeded = () => {
const db = request.result;
for (const store of stores) {
if (!db.objectStoreNames.contains(store)) {
db.createObjectStore(store);
}
}
};
});
}
}
export interface IIndexedDBFileSystemProvider extends Disposable, IFileSystemProviderWithFileReadWriteCapability {
reset(): Promise<void>;
}
class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSystemProvider {
readonly capabilities: FileSystemProviderCapabilities =
FileSystemProviderCapabilities.FileReadWrite
| FileSystemProviderCapabilities.PathCaseSensitive;
readonly onDidChangeCapabilities: Event<void> = Event.None;
private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
readonly onDidChangeFile: Event<readonly IFileChange[]> = this._onDidChangeFile.event;
private readonly versions: Map<string, number> = new Map<string, number>();
private readonly dirs: Set<string> = new Set<string>();
constructor(private readonly scheme: string, private readonly database: IDBDatabase, private readonly store: string) {
super();
this.dirs.add('/');
}
watch(resource: URI, opts: IWatchOptions): IDisposable {
return Disposable.None;
}
async mkdir(resource: URI): Promise<void> {
try {
const resourceStat = await this.stat(resource);
if (resourceStat.type === FileType.File) {
throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory);
}
} catch (error) { /* Ignore */ }
// Make sure parent dir exists
await this.stat(dirname(resource));
this.dirs.add(resource.path);
}
async stat(resource: URI): Promise<IStat> {
try {
const content = await this.readFile(resource);
return {
type: FileType.File,
ctime: 0,
mtime: this.versions.get(resource.toString()) || 0,
size: content.byteLength
};
} catch (e) {
}
const files = await this.readdir(resource);
if (files.length) {
return {
type: FileType.Directory,
ctime: 0,
mtime: 0,
size: 0
};
}
if (this.dirs.has(resource.path)) {
return {
type: FileType.Directory,
ctime: 0,
mtime: 0,
size: 0
};
}
throw createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound);
}
async readdir(resource: URI): Promise<[string, FileType][]> {
const hasKey = await this.hasKey(resource.path);
if (hasKey) {
throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory);
}
const keys = await this.getAllKeys();
const files: Map<string, [string, FileType]> = new Map<string, [string, FileType]>();
for (const key of keys) {
const keyResource = this.toResource(key);
if (extUri.isEqualOrParent(keyResource, resource)) {
const path = extUri.relativePath(resource, keyResource);
if (path) {
const keySegments = path.split('/');
files.set(keySegments[0], [keySegments[0], keySegments.length === 1 ? FileType.File : FileType.Directory]);
}
}
}
return [...files.values()];
}
async readFile(resource: URI): Promise<Uint8Array> {
const hasKey = await this.hasKey(resource.path);
if (!hasKey) {
throw createFileSystemProviderError(localize('fileNotFound', "File not found"), FileSystemProviderErrorCode.FileNotFound);
}
const value = await this.getValue(resource.path);
if (typeof value === 'string') {
return VSBuffer.fromString(value).buffer;
} else {
return value;
}
}
async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
const hasKey = await this.hasKey(resource.path);
if (!hasKey) {
const files = await this.readdir(resource);
if (files.length) {
throw createFileSystemProviderError(localize('fileIsDirectory', "File is Directory"), FileSystemProviderErrorCode.FileIsADirectory);
}
}
await this.setValue(resource.path, content);
this.versions.set(resource.toString(), (this.versions.get(resource.toString()) || 0) + 1);
this._onDidChangeFile.fire([{ resource, type: FileChangeType.UPDATED }]);
}
async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
const hasKey = await this.hasKey(resource.path);
if (hasKey) {
await this.deleteKey(resource.path);
this.versions.delete(resource.path);
this._onDidChangeFile.fire([{ resource, type: FileChangeType.DELETED }]);
return;
}
if (opts.recursive) {
const files = await this.readdir(resource);
await Promise.all(files.map(([key]) => this.delete(joinPath(resource, key), opts)));
}
}
rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
return Promise.reject(new Error('Not Supported'));
}
private toResource(key: string): URI {
return URI.file(key).with({ scheme: this.scheme });
}
async getAllKeys(): Promise<string[]> {
return new Promise(async (c, e) => {
const transaction = this.database.transaction([this.store]);
const objectStore = transaction.objectStore(this.store);
const request = objectStore.getAllKeys();
request.onerror = () => e(request.error);
request.onsuccess = () => c(<string[]>request.result);
});
}
hasKey(key: string): Promise<boolean> {
return new Promise<boolean>(async (c, e) => {
const transaction = this.database.transaction([this.store]);
const objectStore = transaction.objectStore(this.store);
const request = objectStore.getKey(key);
request.onerror = () => e(request.error);
request.onsuccess = () => {
c(!!request.result);
};
});
}
getValue(key: string): Promise<Uint8Array | string> {
return new Promise(async (c, e) => {
const transaction = this.database.transaction([this.store]);
const objectStore = transaction.objectStore(this.store);
const request = objectStore.get(key);
request.onerror = () => e(request.error);
request.onsuccess = () => c(request.result || '');
});
}
setValue(key: string, value: Uint8Array): Promise<void> {
return new Promise(async (c, e) => {
const transaction = this.database.transaction([this.store], 'readwrite');
const objectStore = transaction.objectStore(this.store);
const request = objectStore.put(value, key);
request.onerror = () => e(request.error);
request.onsuccess = () => c();
});
}
deleteKey(key: string): Promise<void> {
return new Promise(async (c, e) => {
const transaction = this.database.transaction([this.store], 'readwrite');
const objectStore = transaction.objectStore(this.store);
const request = objectStore.delete(key);
request.onerror = () => e(request.error);
request.onsuccess = () => c();
});
}
reset(): Promise<void> {
return new Promise(async (c, e) => {
const transaction = this.database.transaction([this.store], 'readwrite');
const objectStore = transaction.objectStore(this.store);
const request = objectStore.clear();
request.onerror = () => e(request.error);
request.onsuccess = () => c();
});
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,230 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import * as resources from 'vs/base/common/resources';
import { FileChangeType, FileType, IWatchOptions, IStat, FileSystemProviderErrorCode, FileSystemProviderError, FileWriteOptions, IFileChange, FileDeleteOptions, FileSystemProviderCapabilities, FileOverwriteOptions, IFileSystemProviderWithFileReadWriteCapability } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
class File implements IStat {
type: FileType.File;
ctime: number;
mtime: number;
size: number;
name: string;
data?: Uint8Array;
constructor(name: string) {
this.type = FileType.File;
this.ctime = Date.now();
this.mtime = Date.now();
this.size = 0;
this.name = name;
}
}
class Directory implements IStat {
type: FileType.Directory;
ctime: number;
mtime: number;
size: number;
name: string;
entries: Map<string, File | Directory>;
constructor(name: string) {
this.type = FileType.Directory;
this.ctime = Date.now();
this.mtime = Date.now();
this.size = 0;
this.name = name;
this.entries = new Map();
}
}
export type Entry = File | Directory;
export class InMemoryFileSystemProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability {
readonly capabilities: FileSystemProviderCapabilities =
FileSystemProviderCapabilities.FileReadWrite
| FileSystemProviderCapabilities.PathCaseSensitive;
readonly onDidChangeCapabilities: Event<void> = Event.None;
root = new Directory('');
// --- manage file metadata
async stat(resource: URI): Promise<IStat> {
return this._lookup(resource, false);
}
async readdir(resource: URI): Promise<[string, FileType][]> {
const entry = this._lookupAsDirectory(resource, false);
let result: [string, FileType][] = [];
entry.entries.forEach((child, name) => result.push([name, child.type]));
return result;
}
// --- manage file contents
async readFile(resource: URI): Promise<Uint8Array> {
const data = this._lookupAsFile(resource, false).data;
if (data) {
return data;
}
throw new FileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);
}
async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
let basename = resources.basename(resource);
let parent = this._lookupParentDirectory(resource);
let entry = parent.entries.get(basename);
if (entry instanceof Directory) {
throw new FileSystemProviderError('file is directory', FileSystemProviderErrorCode.FileIsADirectory);
}
if (!entry && !opts.create) {
throw new FileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);
}
if (entry && opts.create && !opts.overwrite) {
throw new FileSystemProviderError('file exists already', FileSystemProviderErrorCode.FileExists);
}
if (!entry) {
entry = new File(basename);
parent.entries.set(basename, entry);
this._fireSoon({ type: FileChangeType.ADDED, resource });
}
entry.mtime = Date.now();
entry.size = content.byteLength;
entry.data = content;
this._fireSoon({ type: FileChangeType.UPDATED, resource });
}
// --- manage files/folders
async rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
if (!opts.overwrite && this._lookup(to, true)) {
throw new FileSystemProviderError('file exists already', FileSystemProviderErrorCode.FileExists);
}
let entry = this._lookup(from, false);
let oldParent = this._lookupParentDirectory(from);
let newParent = this._lookupParentDirectory(to);
let newName = resources.basename(to);
oldParent.entries.delete(entry.name);
entry.name = newName;
newParent.entries.set(newName, entry);
this._fireSoon(
{ type: FileChangeType.DELETED, resource: from },
{ type: FileChangeType.ADDED, resource: to }
);
}
async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
let dirname = resources.dirname(resource);
let basename = resources.basename(resource);
let parent = this._lookupAsDirectory(dirname, false);
if (parent.entries.has(basename)) {
parent.entries.delete(basename);
parent.mtime = Date.now();
parent.size -= 1;
this._fireSoon({ type: FileChangeType.UPDATED, resource: dirname }, { resource, type: FileChangeType.DELETED });
}
}
async mkdir(resource: URI): Promise<void> {
let basename = resources.basename(resource);
let dirname = resources.dirname(resource);
let parent = this._lookupAsDirectory(dirname, false);
let entry = new Directory(basename);
parent.entries.set(entry.name, entry);
parent.mtime = Date.now();
parent.size += 1;
this._fireSoon({ type: FileChangeType.UPDATED, resource: dirname }, { type: FileChangeType.ADDED, resource });
}
// --- lookup
private _lookup(uri: URI, silent: false): Entry;
private _lookup(uri: URI, silent: boolean): Entry | undefined;
private _lookup(uri: URI, silent: boolean): Entry | undefined {
let parts = uri.path.split('/');
let entry: Entry = this.root;
for (const part of parts) {
if (!part) {
continue;
}
let child: Entry | undefined;
if (entry instanceof Directory) {
child = entry.entries.get(part);
}
if (!child) {
if (!silent) {
throw new FileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);
} else {
return undefined;
}
}
entry = child;
}
return entry;
}
private _lookupAsDirectory(uri: URI, silent: boolean): Directory {
let entry = this._lookup(uri, silent);
if (entry instanceof Directory) {
return entry;
}
throw new FileSystemProviderError('file not a directory', FileSystemProviderErrorCode.FileNotADirectory);
}
private _lookupAsFile(uri: URI, silent: boolean): File {
let entry = this._lookup(uri, silent);
if (entry instanceof File) {
return entry;
}
throw new FileSystemProviderError('file is a directory', FileSystemProviderErrorCode.FileIsADirectory);
}
private _lookupParentDirectory(uri: URI): Directory {
const dirname = resources.dirname(uri);
return this._lookupAsDirectory(dirname, false);
}
// --- manage file events
private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
readonly onDidChangeFile: Event<readonly IFileChange[]> = this._onDidChangeFile.event;
private _bufferedChanges: IFileChange[] = [];
private _fireSoonHandle?: any;
watch(resource: URI, opts: IWatchOptions): IDisposable {
// ignore, fires for all changes...
return Disposable.None;
}
private _fireSoon(...changes: IFileChange[]): void {
this._bufferedChanges.push(...changes);
if (this._fireSoonHandle) {
clearTimeout(this._fireSoonHandle);
}
this._fireSoonHandle = setTimeout(() => {
this._onDidChangeFile.fire(this._bufferedChanges);
this._bufferedChanges.length = 0;
}, 5);
}
}

View File

@@ -0,0 +1,134 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { URI } from 'vs/base/common/uri';
import { VSBuffer } from 'vs/base/common/buffer';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, createFileSystemProviderError, FileSystemProviderErrorCode, ensureFileSystemProviderError } from 'vs/platform/files/common/files';
import { canceled } from 'vs/base/common/errors';
import { IErrorTransformer, IDataTransformer, WriteableStream } from 'vs/base/common/stream';
export interface ICreateReadStreamOptions extends FileReadStreamOptions {
/**
* The size of the buffer to use before sending to the stream.
*/
bufferSize: number;
/**
* Allows to massage any possibly error that happens during reading.
*/
errorTransformer?: IErrorTransformer;
}
/**
* A helper to read a file from a provider with open/read/close capability into a stream.
*/
export async function readFileIntoStream<T>(
provider: IFileSystemProviderWithOpenReadWriteCloseCapability,
resource: URI,
target: WriteableStream<T>,
transformer: IDataTransformer<VSBuffer, T>,
options: ICreateReadStreamOptions,
token: CancellationToken
): Promise<void> {
let error: Error | undefined = undefined;
try {
await doReadFileIntoStream(provider, resource, target, transformer, options, token);
} catch (err) {
error = err;
} finally {
if (error && options.errorTransformer) {
error = options.errorTransformer(error);
}
target.end(error);
}
}
async function doReadFileIntoStream<T>(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, target: WriteableStream<T>, transformer: IDataTransformer<VSBuffer, T>, options: ICreateReadStreamOptions, token: CancellationToken): Promise<void> {
// Check for cancellation
throwIfCancelled(token);
// open handle through provider
const handle = await provider.open(resource, { create: false });
// Check for cancellation
throwIfCancelled(token);
try {
let totalBytesRead = 0;
let bytesRead = 0;
let allowedRemainingBytes = (options && typeof options.length === 'number') ? options.length : undefined;
let buffer = VSBuffer.alloc(Math.min(options.bufferSize, typeof allowedRemainingBytes === 'number' ? allowedRemainingBytes : options.bufferSize));
let posInFile = options && typeof options.position === 'number' ? options.position : 0;
let posInBuffer = 0;
do {
// read from source (handle) at current position (pos) into buffer (buffer) at
// buffer position (posInBuffer) up to the size of the buffer (buffer.byteLength).
bytesRead = await provider.read(handle, posInFile, buffer.buffer, posInBuffer, buffer.byteLength - posInBuffer);
posInFile += bytesRead;
posInBuffer += bytesRead;
totalBytesRead += bytesRead;
if (typeof allowedRemainingBytes === 'number') {
allowedRemainingBytes -= bytesRead;
}
// when buffer full, create a new one and emit it through stream
if (posInBuffer === buffer.byteLength) {
await target.write(transformer(buffer));
buffer = VSBuffer.alloc(Math.min(options.bufferSize, typeof allowedRemainingBytes === 'number' ? allowedRemainingBytes : options.bufferSize));
posInBuffer = 0;
}
} while (bytesRead > 0 && (typeof allowedRemainingBytes !== 'number' || allowedRemainingBytes > 0) && throwIfCancelled(token) && throwIfTooLarge(totalBytesRead, options));
// wrap up with last buffer (also respect maxBytes if provided)
if (posInBuffer > 0) {
let lastChunkLength = posInBuffer;
if (typeof allowedRemainingBytes === 'number') {
lastChunkLength = Math.min(posInBuffer, allowedRemainingBytes);
}
target.write(transformer(buffer.slice(0, lastChunkLength)));
}
} catch (error) {
throw ensureFileSystemProviderError(error);
} finally {
await provider.close(handle);
}
}
function throwIfCancelled(token: CancellationToken): boolean {
if (token.isCancellationRequested) {
throw canceled();
}
return true;
}
function throwIfTooLarge(totalBytesRead: number, options: ICreateReadStreamOptions): boolean {
// Return early if file is too large to load and we have configured limits
if (options?.limits) {
if (typeof options.limits.memory === 'number' && totalBytesRead > options.limits.memory) {
throw createFileSystemProviderError(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow it to use more memory"), FileSystemProviderErrorCode.FileExceedsMemoryLimit);
}
if (typeof options.limits.size === 'number' && totalBytesRead > options.limits.size) {
throw createFileSystemProviderError(localize('fileTooLargeError', "File is too large to open"), FileSystemProviderErrorCode.FileTooLarge);
}
}
return true;
}

View File

@@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DiskFileSystemProvider as NodeDiskFileSystemProvider, IDiskFileSystemProviderOptions } from 'vs/platform/files/node/diskFileSystemProvider';
import { FileDeleteOptions, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
import { isWindows } from 'vs/base/common/platform';
import { localize } from 'vs/nls';
import { basename } from 'vs/base/common/path';
import { ILogService } from 'vs/platform/log/common/log';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
export class DiskFileSystemProvider extends NodeDiskFileSystemProvider {
constructor(
logService: ILogService,
private readonly nativeHostService: INativeHostService,
options?: IDiskFileSystemProviderOptions
) {
super(logService, options);
}
get capabilities(): FileSystemProviderCapabilities {
if (!this._capabilities) {
this._capabilities = super.capabilities | FileSystemProviderCapabilities.Trash;
}
return this._capabilities;
}
protected async doDelete(filePath: string, opts: FileDeleteOptions): Promise<void> {
if (!opts.useTrash) {
return super.doDelete(filePath, opts);
}
const result = await this.nativeHostService.moveItemToTrash(filePath);
if (!result) {
throw new Error(isWindows ? localize('binFailed', "Failed to move '{0}' to the recycle bin", basename(filePath)) : localize('trashFailed', "Failed to move '{0}' to the trash", basename(filePath)));
}
}
}

View File

@@ -0,0 +1,702 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { mkdir, open, close, read, write, fdatasync, Dirent, Stats } from 'fs';
import { promisify } from 'util';
import { IDisposable, Disposable, toDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle';
import { FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, IFileSystemProviderWithFileFolderCopyCapability } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { isLinux, isWindows } from 'vs/base/common/platform';
import { statLink, unlink, move, copy, readFile, truncate, rimraf, RimRafMode, exists, readdirWithFileTypes } from 'vs/base/node/pfs';
import { normalize, basename, dirname } from 'vs/base/common/path';
import { joinPath } from 'vs/base/common/resources';
import { isEqual } from 'vs/base/common/extpath';
import { retry, ThrottledDelayer } from 'vs/base/common/async';
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
import { localize } from 'vs/nls';
import { IDiskFileChange, toFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { FileWatcher as UnixWatcherService } from 'vs/platform/files/node/watcher/unix/watcherService';
import { FileWatcher as WindowsWatcherService } from 'vs/platform/files/node/watcher/win32/watcherService';
import { FileWatcher as NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/watcherService';
import { FileWatcher as NodeJSWatcherService } from 'vs/platform/files/node/watcher/nodejs/watcherService';
import { CancellationToken } from 'vs/base/common/cancellation';
import { ReadableStreamEvents, newWriteableStream } from 'vs/base/common/stream';
import { readFileIntoStream } from 'vs/platform/files/common/io';
import { insert } from 'vs/base/common/arrays';
import { VSBuffer } from 'vs/base/common/buffer';
export interface IWatcherOptions {
pollingInterval?: number;
usePolling: boolean;
}
export interface IDiskFileSystemProviderOptions {
bufferSize?: number;
watcher?: IWatcherOptions;
}
export class DiskFileSystemProvider extends Disposable implements
IFileSystemProviderWithFileReadWriteCapability,
IFileSystemProviderWithOpenReadWriteCloseCapability,
IFileSystemProviderWithFileReadStreamCapability,
IFileSystemProviderWithFileFolderCopyCapability {
private readonly BUFFER_SIZE = this.options?.bufferSize || 64 * 1024;
constructor(
private readonly logService: ILogService,
private readonly options?: IDiskFileSystemProviderOptions
) {
super();
}
//#region File Capabilities
onDidChangeCapabilities: Event<void> = Event.None;
protected _capabilities: FileSystemProviderCapabilities | undefined;
get capabilities(): FileSystemProviderCapabilities {
if (!this._capabilities) {
this._capabilities =
FileSystemProviderCapabilities.FileReadWrite |
FileSystemProviderCapabilities.FileOpenReadWriteClose |
FileSystemProviderCapabilities.FileReadStream |
FileSystemProviderCapabilities.FileFolderCopy;
if (isLinux) {
this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
}
}
return this._capabilities;
}
//#endregion
//#region File Metadata Resolving
async stat(resource: URI): Promise<IStat> {
try {
const { stat, symbolicLink } = await statLink(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly
return {
type: this.toType(stat, symbolicLink),
ctime: stat.birthtime.getTime(), // intentionally not using ctime here, we want the creation time
mtime: stat.mtime.getTime(),
size: stat.size
};
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
async readdir(resource: URI): Promise<[string, FileType][]> {
try {
const children = await readdirWithFileTypes(this.toFilePath(resource));
const result: [string, FileType][] = [];
await Promise.all(children.map(async child => {
try {
let type: FileType;
if (child.isSymbolicLink()) {
type = (await this.stat(joinPath(resource, child.name))).type; // always resolve target the link points to if any
} else {
type = this.toType(child);
}
result.push([child.name, type]);
} catch (error) {
this.logService.trace(error); // ignore errors for individual entries that can arise from permission denied
}
}));
return result;
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
private toType(entry: Stats | Dirent, symbolicLink?: { dangling: boolean }): FileType {
// Signal file type by checking for file / directory, except:
// - symbolic links pointing to non-existing files are FileType.Unknown
// - files that are neither file nor directory are FileType.Unknown
let type: FileType;
if (symbolicLink?.dangling) {
type = FileType.Unknown;
} else if (entry.isFile()) {
type = FileType.File;
} else if (entry.isDirectory()) {
type = FileType.Directory;
} else {
type = FileType.Unknown;
}
// Always signal symbolic link as file type additionally
if (symbolicLink) {
type |= FileType.SymbolicLink;
}
return type;
}
//#endregion
//#region File Reading/Writing
async readFile(resource: URI): Promise<Uint8Array> {
try {
const filePath = this.toFilePath(resource);
return await readFile(filePath);
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
readFileStream(resource: URI, opts: FileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {
const stream = newWriteableStream<Uint8Array>(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer);
readFileIntoStream(this, resource, stream, data => data.buffer, {
...opts,
bufferSize: this.BUFFER_SIZE
}, token);
return stream;
}
async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
let handle: number | undefined = undefined;
try {
const filePath = this.toFilePath(resource);
// Validate target unless { create: true, overwrite: true }
if (!opts.create || !opts.overwrite) {
const fileExists = await exists(filePath);
if (fileExists) {
if (!opts.overwrite) {
throw createFileSystemProviderError(localize('fileExists', "File already exists"), FileSystemProviderErrorCode.FileExists);
}
} else {
if (!opts.create) {
throw createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound);
}
}
}
// Open
handle = await this.open(resource, { create: true });
// Write content at once
await this.write(handle, 0, content, 0, content.byteLength);
} catch (error) {
throw this.toFileSystemProviderError(error);
} finally {
if (typeof handle === 'number') {
await this.close(handle);
}
}
}
private readonly mapHandleToPos: Map<number, number> = new Map();
private readonly writeHandles: Set<number> = new Set();
private canFlush: boolean = true;
async open(resource: URI, opts: FileOpenOptions): Promise<number> {
try {
const filePath = this.toFilePath(resource);
let flags: string | undefined = undefined;
if (opts.create) {
if (isWindows && await exists(filePath)) {
try {
// On Windows and if the file exists, we use a different strategy of saving the file
// by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows
// (see https://github.com/microsoft/vscode/issues/931) and prevent removing alternate data streams
// (see https://github.com/microsoft/vscode/issues/6363)
await truncate(filePath, 0);
// After a successful truncate() the flag can be set to 'r+' which will not truncate.
flags = 'r+';
} catch (error) {
this.logService.trace(error);
}
}
// we take opts.create as a hint that the file is opened for writing
// as such we use 'w' to truncate an existing or create the
// file otherwise. we do not allow reading.
if (!flags) {
flags = 'w';
}
} else {
// otherwise we assume the file is opened for reading
// as such we use 'r' to neither truncate, nor create
// the file.
flags = 'r';
}
const handle = await promisify(open)(filePath, flags);
// remember this handle to track file position of the handle
// we init the position to 0 since the file descriptor was
// just created and the position was not moved so far (see
// also http://man7.org/linux/man-pages/man2/open.2.html -
// "The file offset is set to the beginning of the file.")
this.mapHandleToPos.set(handle, 0);
// remember that this handle was used for writing
if (opts.create) {
this.writeHandles.add(handle);
}
return handle;
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
async close(fd: number): Promise<void> {
try {
// remove this handle from map of positions
this.mapHandleToPos.delete(fd);
// if a handle is closed that was used for writing, ensure
// to flush the contents to disk if possible.
if (this.writeHandles.delete(fd) && this.canFlush) {
try {
await promisify(fdatasync)(fd);
} catch (error) {
// In some exotic setups it is well possible that node fails to sync
// In that case we disable flushing and log the error to our logger
this.canFlush = false;
this.logService.error(error);
}
}
return await promisify(close)(fd);
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
const normalizedPos = this.normalizePos(fd, pos);
let bytesRead: number | null = null;
try {
const result = await promisify(read)(fd, data, offset, length, normalizedPos);
if (typeof result === 'number') {
bytesRead = result; // node.d.ts fail
} else {
bytesRead = result.bytesRead;
}
return bytesRead;
} catch (error) {
throw this.toFileSystemProviderError(error);
} finally {
this.updatePos(fd, normalizedPos, bytesRead);
}
}
private normalizePos(fd: number, pos: number): number | null {
// when calling fs.read/write we try to avoid passing in the "pos" argument and
// rather prefer to pass in "null" because this avoids an extra seek(pos)
// call that in some cases can even fail (e.g. when opening a file over FTP -
// see https://github.com/microsoft/vscode/issues/73884).
//
// as such, we compare the passed in position argument with our last known
// position for the file descriptor and use "null" if they match.
if (pos === this.mapHandleToPos.get(fd)) {
return null;
}
return pos;
}
private updatePos(fd: number, pos: number | null, bytesLength: number | null): void {
const lastKnownPos = this.mapHandleToPos.get(fd);
if (typeof lastKnownPos === 'number') {
// pos !== null signals that previously a position was used that is
// not null. node.js documentation explains, that in this case
// the internal file pointer is not moving and as such we do not move
// our position pointer.
//
// Docs: "If position is null, data will be read from the current file position,
// and the file position will be updated. If position is an integer, the file position
// will remain unchanged."
if (typeof pos === 'number') {
// do not modify the position
}
// bytesLength = number is a signal that the read/write operation was
// successful and as such we need to advance the position in the Map
//
// Docs (http://man7.org/linux/man-pages/man2/read.2.html):
// "On files that support seeking, the read operation commences at the
// file offset, and the file offset is incremented by the number of
// bytes read."
//
// Docs (http://man7.org/linux/man-pages/man2/write.2.html):
// "For a seekable file (i.e., one to which lseek(2) may be applied, for
// example, a regular file) writing takes place at the file offset, and
// the file offset is incremented by the number of bytes actually
// written."
else if (typeof bytesLength === 'number') {
this.mapHandleToPos.set(fd, lastKnownPos + bytesLength);
}
// bytesLength = null signals an error in the read/write operation
// and as such we drop the handle from the Map because the position
// is unspecificed at this point.
else {
this.mapHandleToPos.delete(fd);
}
}
}
async write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
// we know at this point that the file to write to is truncated and thus empty
// if the write now fails, the file remains empty. as such we really try hard
// to ensure the write succeeds by retrying up to three times.
return retry(() => this.doWrite(fd, pos, data, offset, length), 100 /* ms delay */, 3 /* retries */);
}
private async doWrite(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
const normalizedPos = this.normalizePos(fd, pos);
let bytesWritten: number | null = null;
try {
const result = await promisify(write)(fd, data, offset, length, normalizedPos);
if (typeof result === 'number') {
bytesWritten = result; // node.d.ts fail
} else {
bytesWritten = result.bytesWritten;
}
return bytesWritten;
} catch (error) {
throw this.toFileSystemProviderError(error);
} finally {
this.updatePos(fd, normalizedPos, bytesWritten);
}
}
//#endregion
//#region Move/Copy/Delete/Create Folder
async mkdir(resource: URI): Promise<void> {
try {
await promisify(mkdir)(this.toFilePath(resource));
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
try {
const filePath = this.toFilePath(resource);
await this.doDelete(filePath, opts);
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
protected async doDelete(filePath: string, opts: FileDeleteOptions): Promise<void> {
if (opts.recursive) {
await rimraf(filePath, RimRafMode.MOVE);
} else {
await unlink(filePath);
}
}
async rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
const fromFilePath = this.toFilePath(from);
const toFilePath = this.toFilePath(to);
if (fromFilePath === toFilePath) {
return; // simulate node.js behaviour here and do a no-op if paths match
}
try {
// Ensure target does not exist
await this.validateTargetDeleted(from, to, 'move', opts.overwrite);
// Move
await move(fromFilePath, toFilePath);
} catch (error) {
// rewrite some typical errors that can happen especially around symlinks
// to something the user can better understand
if (error.code === 'EINVAL' || error.code === 'EBUSY' || error.code === 'ENAMETOOLONG') {
error = new Error(localize('moveError', "Unable to move '{0}' into '{1}' ({2}).", basename(fromFilePath), basename(dirname(toFilePath)), error.toString()));
}
throw this.toFileSystemProviderError(error);
}
}
async copy(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
const fromFilePath = this.toFilePath(from);
const toFilePath = this.toFilePath(to);
if (fromFilePath === toFilePath) {
return; // simulate node.js behaviour here and do a no-op if paths match
}
try {
// Ensure target does not exist
await this.validateTargetDeleted(from, to, 'copy', opts.overwrite);
// Copy
await copy(fromFilePath, toFilePath);
} catch (error) {
// rewrite some typical errors that can happen especially around symlinks
// to something the user can better understand
if (error.code === 'EINVAL' || error.code === 'EBUSY' || error.code === 'ENAMETOOLONG') {
error = new Error(localize('copyError', "Unable to copy '{0}' into '{1}' ({2}).", basename(fromFilePath), basename(dirname(toFilePath)), error.toString()));
}
throw this.toFileSystemProviderError(error);
}
}
private async validateTargetDeleted(from: URI, to: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<void> {
const fromFilePath = this.toFilePath(from);
const toFilePath = this.toFilePath(to);
let isSameResourceWithDifferentPathCase = false;
const isPathCaseSensitive = !!(this.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);
if (!isPathCaseSensitive) {
isSameResourceWithDifferentPathCase = isEqual(fromFilePath, toFilePath, true /* ignore case */);
}
if (isSameResourceWithDifferentPathCase && mode === 'copy') {
throw createFileSystemProviderError(localize('fileCopyErrorPathCase', "'File cannot be copied to same path with different path case"), FileSystemProviderErrorCode.FileExists);
}
// handle existing target (unless this is a case change)
if (!isSameResourceWithDifferentPathCase && await exists(toFilePath)) {
if (!overwrite) {
throw createFileSystemProviderError(localize('fileCopyErrorExists', "File at target already exists"), FileSystemProviderErrorCode.FileExists);
}
// Delete target
await this.delete(to, { recursive: true, useTrash: false });
}
}
//#endregion
//#region File Watching
private readonly _onDidWatchErrorOccur = this._register(new Emitter<string>());
readonly onDidErrorOccur = this._onDidWatchErrorOccur.event;
private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
readonly onDidChangeFile = this._onDidChangeFile.event;
private recursiveWatcher: WindowsWatcherService | UnixWatcherService | NsfwWatcherService | undefined;
private readonly recursiveFoldersToWatch: { path: string, excludes: string[] }[] = [];
private recursiveWatchRequestDelayer = this._register(new ThrottledDelayer<void>(0));
private recursiveWatcherLogLevelListener: IDisposable | undefined;
watch(resource: URI, opts: IWatchOptions): IDisposable {
if (opts.recursive) {
return this.watchRecursive(resource, opts.excludes);
}
return this.watchNonRecursive(resource); // TODO@ben ideally the same watcher can be used in both cases
}
private watchRecursive(resource: URI, excludes: string[]): IDisposable {
// Add to list of folders to watch recursively
const folderToWatch = { path: this.toFilePath(resource), excludes };
const remove = insert(this.recursiveFoldersToWatch, folderToWatch);
// Trigger update
this.refreshRecursiveWatchers();
return toDisposable(() => {
// Remove from list of folders to watch recursively
remove();
// Trigger update
this.refreshRecursiveWatchers();
});
}
private refreshRecursiveWatchers(): void {
// Buffer requests for recursive watching to decide on right watcher
// that supports potentially watching more than one folder at once
this.recursiveWatchRequestDelayer.trigger(async () => {
this.doRefreshRecursiveWatchers();
});
}
private doRefreshRecursiveWatchers(): void {
// Reuse existing
if (this.recursiveWatcher instanceof NsfwWatcherService) {
this.recursiveWatcher.setFolders(this.recursiveFoldersToWatch);
}
// Create new
else {
// Dispose old
dispose(this.recursiveWatcher);
this.recursiveWatcher = undefined;
// Create new if we actually have folders to watch
if (this.recursiveFoldersToWatch.length > 0) {
let watcherImpl: {
new(
folders: { path: string, excludes: string[] }[],
onChange: (changes: IDiskFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean,
watcherOptions?: IWatcherOptions
): WindowsWatcherService | UnixWatcherService | NsfwWatcherService
};
let watcherOptions: IWatcherOptions | undefined = undefined;
// requires a polling watcher
if (this.options?.watcher?.usePolling) {
watcherImpl = UnixWatcherService;
watcherOptions = this.options?.watcher;
}
// Single Folder Watcher
else {
if (this.recursiveFoldersToWatch.length === 1) {
if (isWindows) {
watcherImpl = WindowsWatcherService;
} else {
watcherImpl = UnixWatcherService;
}
}
// Multi Folder Watcher
else {
watcherImpl = NsfwWatcherService;
}
}
// Create and start watching
this.recursiveWatcher = new watcherImpl(
this.recursiveFoldersToWatch,
event => this._onDidChangeFile.fire(toFileChanges(event)),
msg => {
if (msg.type === 'error') {
this._onDidWatchErrorOccur.fire(msg.message);
}
this.logService[msg.type](msg.message);
},
this.logService.getLevel() === LogLevel.Trace,
watcherOptions
);
if (!this.recursiveWatcherLogLevelListener) {
this.recursiveWatcherLogLevelListener = this.logService.onDidChangeLogLevel(() => {
if (this.recursiveWatcher) {
this.recursiveWatcher.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace);
}
});
}
}
}
}
private watchNonRecursive(resource: URI): IDisposable {
const watcherService = new NodeJSWatcherService(
this.toFilePath(resource),
changes => this._onDidChangeFile.fire(toFileChanges(changes)),
msg => {
if (msg.type === 'error') {
this._onDidWatchErrorOccur.fire(msg.message);
}
this.logService[msg.type](msg.message);
},
this.logService.getLevel() === LogLevel.Trace
);
const logLevelListener = this.logService.onDidChangeLogLevel(() => {
watcherService.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace);
});
return combinedDisposable(watcherService, logLevelListener);
}
//#endregion
//#region Helpers
protected toFilePath(resource: URI): string {
return normalize(resource.fsPath);
}
private toFileSystemProviderError(error: NodeJS.ErrnoException): FileSystemProviderError {
if (error instanceof FileSystemProviderError) {
return error; // avoid double conversion
}
let code: FileSystemProviderErrorCode;
switch (error.code) {
case 'ENOENT':
code = FileSystemProviderErrorCode.FileNotFound;
break;
case 'EISDIR':
code = FileSystemProviderErrorCode.FileIsADirectory;
break;
case 'ENOTDIR':
code = FileSystemProviderErrorCode.FileNotADirectory;
break;
case 'EEXIST':
code = FileSystemProviderErrorCode.FileExists;
break;
case 'EPERM':
case 'EACCES':
code = FileSystemProviderErrorCode.NoPermissions;
break;
default:
code = FileSystemProviderErrorCode.Unknown;
}
return createFileSystemProviderError(error, code);
}
//#endregion
dispose(): void {
super.dispose();
dispose(this.recursiveWatcher);
this.recursiveWatcher = undefined;
dispose(this.recursiveWatcherLogLevelListener);
this.recursiveWatcherLogLevelListener = undefined;
}
}

View File

@@ -0,0 +1,126 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { Disposable } from 'vs/base/common/lifecycle';
import { statLink } from 'vs/base/node/pfs';
import { realpath } from 'vs/base/node/extpath';
import { watchFolder, watchFile, CHANGE_BUFFER_DELAY } from 'vs/base/node/watcher';
import { FileChangeType } from 'vs/platform/files/common/files';
import { ThrottledDelayer } from 'vs/base/common/async';
import { join, basename } from 'vs/base/common/path';
export class FileWatcher extends Disposable {
private isDisposed: boolean | undefined;
private fileChangesDelayer: ThrottledDelayer<void> = this._register(new ThrottledDelayer<void>(CHANGE_BUFFER_DELAY * 2 /* sync on delay from underlying library */));
private fileChangesBuffer: IDiskFileChange[] = [];
constructor(
private path: string,
private onDidFilesChange: (changes: IDiskFileChange[]) => void,
private onLogMessage: (msg: ILogMessage) => void,
private verboseLogging: boolean
) {
super();
this.startWatching();
}
setVerboseLogging(verboseLogging: boolean): void {
this.verboseLogging = verboseLogging;
}
private async startWatching(): Promise<void> {
try {
const { stat, symbolicLink } = await statLink(this.path);
if (this.isDisposed) {
return;
}
let pathToWatch = this.path;
if (symbolicLink) {
try {
pathToWatch = await realpath(pathToWatch);
} catch (error) {
this.onError(error);
}
}
// Watch Folder
if (stat.isDirectory()) {
this._register(watchFolder(pathToWatch, (eventType, path) => {
this.onFileChange({
type: eventType === 'changed' ? FileChangeType.UPDATED : eventType === 'added' ? FileChangeType.ADDED : FileChangeType.DELETED,
path: join(this.path, basename(path)) // ensure path is identical with what was passed in
});
}, error => this.onError(error)));
}
// Watch File
else {
this._register(watchFile(pathToWatch, eventType => {
this.onFileChange({
type: eventType === 'changed' ? FileChangeType.UPDATED : FileChangeType.DELETED,
path: this.path // ensure path is identical with what was passed in
});
}, error => this.onError(error)));
}
} catch (error) {
this.onError(error);
}
}
private onFileChange(event: IDiskFileChange): void {
// Add to buffer
this.fileChangesBuffer.push(event);
// Logging
if (this.verboseLogging) {
this.onVerbose(`${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`);
}
// Handle emit through delayer to accommodate for bulk changes and thus reduce spam
this.fileChangesDelayer.trigger(async () => {
const fileChanges = this.fileChangesBuffer;
this.fileChangesBuffer = [];
// Event normalization
const normalizedFileChanges = normalizeFileChanges(fileChanges);
// Logging
if (this.verboseLogging) {
normalizedFileChanges.forEach(event => {
this.onVerbose(`>> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`);
});
}
// Fire
if (normalizedFileChanges.length > 0) {
this.onDidFilesChange(normalizedFileChanges);
}
});
}
private onError(error: string): void {
if (!this.isDisposed) {
this.onLogMessage({ type: 'error', message: `[File Watcher (node.js)] ${error}` });
}
}
private onVerbose(message: string): void {
if (!this.isDisposed) {
this.onLogMessage({ type: 'trace', message: `[File Watcher (node.js)] ${message}` });
}
}
dispose(): void {
this.isDisposed = true;
super.dispose();
}
}

View File

@@ -0,0 +1,252 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as glob from 'vs/base/common/glob';
import * as extpath from 'vs/base/common/extpath';
import * as path from 'vs/base/common/path';
import * as platform from 'vs/base/common/platform';
import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import * as nsfw from 'vscode-nsfw';
import { IWatcherService, IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher';
import { ThrottledDelayer } from 'vs/base/common/async';
import { FileChangeType } from 'vs/platform/files/common/files';
import { normalizeNFC } from 'vs/base/common/normalization';
import { Event, Emitter } from 'vs/base/common/event';
import { realcaseSync, realpathSync } from 'vs/base/node/extpath';
import { Disposable } from 'vs/base/common/lifecycle';
const nsfwActionToRawChangeType: { [key: number]: number } = [];
nsfwActionToRawChangeType[nsfw.actions.CREATED] = FileChangeType.ADDED;
nsfwActionToRawChangeType[nsfw.actions.MODIFIED] = FileChangeType.UPDATED;
nsfwActionToRawChangeType[nsfw.actions.DELETED] = FileChangeType.DELETED;
interface IWatcherObjet {
start(): void;
stop(): void;
}
interface IPathWatcher {
ready: Promise<IWatcherObjet>;
watcher?: IWatcherObjet;
ignored: glob.ParsedPattern[];
}
export class NsfwWatcherService extends Disposable implements IWatcherService {
private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms)
private readonly _onDidChangeFile = this._register(new Emitter<IDiskFileChange[]>());
readonly onDidChangeFile = this._onDidChangeFile.event;
private readonly _onDidLogMessage = this._register(new Emitter<ILogMessage>());
readonly onDidLogMessage: Event<ILogMessage> = this._onDidLogMessage.event;
private pathWatchers: { [watchPath: string]: IPathWatcher } = {};
private verboseLogging: boolean | undefined;
private enospcErrorLogged: boolean | undefined;
async setRoots(roots: IWatcherRequest[]): Promise<void> {
const normalizedRoots = this._normalizeRoots(roots);
// Gather roots that are not currently being watched
const rootsToStartWatching = normalizedRoots.filter(r => {
return !(r.path in this.pathWatchers);
});
// Gather current roots that don't exist in the new roots array
const rootsToStopWatching = Object.keys(this.pathWatchers).filter(r => {
return normalizedRoots.every(normalizedRoot => normalizedRoot.path !== r);
});
// Logging
if (this.verboseLogging) {
this.log(`Start watching: [${rootsToStartWatching.map(r => r.path).join(',')}]\nStop watching: [${rootsToStopWatching.join(',')}]`);
}
// Stop watching some roots
rootsToStopWatching.forEach(root => {
this.pathWatchers[root].ready.then(watcher => watcher.stop());
delete this.pathWatchers[root];
});
// Start watching some roots
rootsToStartWatching.forEach(root => this.doWatch(root));
// Refresh ignored arrays in case they changed
roots.forEach(root => {
if (root.path in this.pathWatchers) {
this.pathWatchers[root.path].ignored = Array.isArray(root.excludes) ? root.excludes.map(ignored => glob.parse(ignored)) : [];
}
});
}
private doWatch(request: IWatcherRequest): void {
let undeliveredFileEvents: IDiskFileChange[] = [];
const fileEventDelayer = new ThrottledDelayer<void>(NsfwWatcherService.FS_EVENT_DELAY);
let readyPromiseResolve: (watcher: IWatcherObjet) => void;
this.pathWatchers[request.path] = {
ready: new Promise<IWatcherObjet>(resolve => readyPromiseResolve = resolve),
ignored: Array.isArray(request.excludes) ? request.excludes.map(ignored => glob.parse(ignored)) : []
};
process.on('uncaughtException', (e: Error | string) => {
// Specially handle ENOSPC errors that can happen when
// the watcher consumes so many file descriptors that
// we are running into a limit. We only want to warn
// once in this case to avoid log spam.
// See https://github.com/microsoft/vscode/issues/7950
if (e === 'Inotify limit reached' && !this.enospcErrorLogged) {
this.enospcErrorLogged = true;
this.error('Inotify limit reached (ENOSPC)');
}
});
// NSFW does not report file changes in the path provided on macOS if
// - the path uses wrong casing
// - the path is a symbolic link
// We have to detect this case and massage the events to correct this.
let realBasePathDiffers = false;
let realBasePathLength = request.path.length;
if (platform.isMacintosh) {
try {
// First check for symbolic link
let realBasePath = realpathSync(request.path);
// Second check for casing difference
if (request.path === realBasePath) {
realBasePath = (realcaseSync(request.path) || request.path);
}
if (request.path !== realBasePath) {
realBasePathLength = realBasePath.length;
realBasePathDiffers = true;
this.warn(`Watcher basePath does not match version on disk and will be corrected (original: ${request.path}, real: ${realBasePath})`);
}
} catch (error) {
// ignore
}
}
if (this.verboseLogging) {
this.log(`Start watching with nsfw: ${request.path}`);
}
nsfw(request.path, events => {
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 || '');
this.log(`${e.action === nsfw.actions.CREATED ? '[CREATED]' : e.action === nsfw.actions.DELETED ? '[DELETED]' : e.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]'} ${logPath}`);
}
// Convert nsfw event to IRawFileChange and add to queue
let absolutePath: string;
if (e.action === nsfw.actions.RENAMED) {
// Rename fires when a file's name changes within a single directory
absolutePath = path.join(e.directory, e.oldFile || '');
if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) {
undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath });
} else if (this.verboseLogging) {
this.log(` >> ignored ${absolutePath}`);
}
absolutePath = path.join(e.newDirectory || e.directory, e.newFile || '');
if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) {
undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath });
} else if (this.verboseLogging) {
this.log(` >> ignored ${absolutePath}`);
}
} else {
absolutePath = path.join(e.directory, e.file || '');
if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) {
undeliveredFileEvents.push({
type: nsfwActionToRawChangeType[e.action],
path: absolutePath
});
} else if (this.verboseLogging) {
this.log(` >> ignored ${absolutePath}`);
}
}
}
// Delay and send buffer
fileEventDelayer.trigger(async () => {
const events = undeliveredFileEvents;
undeliveredFileEvents = [];
if (platform.isMacintosh) {
events.forEach(e => {
// Mac uses NFD unicode form on disk, but we want NFC
e.path = normalizeNFC(e.path);
// Convert paths back to original form in case it differs
if (realBasePathDiffers) {
e.path = request.path + e.path.substr(realBasePathLength);
}
});
}
// Broadcast to clients normalized
const res = normalizeFileChanges(events);
this._onDidChangeFile.fire(res);
// Logging
if (this.verboseLogging) {
res.forEach(r => {
this.log(` >> normalized ${r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${r.path}`);
});
}
});
}).then(watcher => {
this.pathWatchers[request.path].watcher = watcher;
const startPromise = watcher.start();
startPromise.then(() => readyPromiseResolve(watcher));
return startPromise;
});
}
async setVerboseLogging(enabled: boolean): Promise<void> {
this.verboseLogging = enabled;
}
async stop(): Promise<void> {
for (let path in this.pathWatchers) {
let watcher = this.pathWatchers[path];
watcher.ready.then(watcher => watcher.stop());
delete this.pathWatchers[path];
}
this.pathWatchers = Object.create(null);
}
protected _normalizeRoots(roots: IWatcherRequest[]): IWatcherRequest[] {
// Normalizes a set of root paths by removing any root paths that are
// sub-paths of other roots.
return roots.filter(r => roots.every(other => {
return !(r.path.length > other.path.length && extpath.isEqualOrParent(r.path, other.path));
}));
}
private isPathIgnored(absolutePath: string, ignored: glob.ParsedPattern[]): boolean {
return ignored && ignored.some(i => i(absolutePath));
}
private log(message: string) {
this._onDidLogMessage.fire({ type: 'trace', message: `[File Watcher (nsfw)] ` + message });
}
private warn(message: string) {
this._onDidLogMessage.fire({ type: 'warn', message: `[File Watcher (nsfw)] ` + message });
}
private error(message: string) {
this._onDidLogMessage.fire({ type: 'error', message: `[File Watcher (nsfw)] ` + message });
}
}

View File

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

View File

@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
export interface IWatcherRequest {
path: string;
excludes: string[];
}
export interface IWatcherService {
readonly onDidChangeFile: Event<IDiskFileChange[]>;
readonly onDidLogMessage: Event<ILogMessage>;
setRoots(roots: IWatcherRequest[]): Promise<void>;
setVerboseLogging(enabled: boolean): Promise<void>;
stop(): Promise<void>;
}

View File

@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
import { NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/nsfwWatcherService';
import { createChannelReceiver } from 'vs/base/parts/ipc/common/ipc';
const server = new Server('watcher');
const service = new NsfwWatcherService();
server.registerChannel('watcher', createChannelReceiver(service));

View File

@@ -0,0 +1,99 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createChannelSender, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { Disposable } from 'vs/base/common/lifecycle';
import { IWatcherRequest, IWatcherService } from 'vs/platform/files/node/watcher/nsfw/watcher';
import { FileAccess } from 'vs/base/common/network';
export class FileWatcher extends Disposable {
private static readonly MAX_RESTARTS = 5;
private service: IWatcherService | undefined;
private isDisposed: boolean;
private restartCounter: number;
constructor(
private folders: IWatcherRequest[],
private onDidFilesChange: (changes: IDiskFileChange[]) => void,
private onLogMessage: (msg: ILogMessage) => void,
private verboseLogging: boolean,
) {
super();
this.isDisposed = false;
this.restartCounter = 0;
this.startWatching();
}
private startWatching(): void {
const client = this._register(new Client(
FileAccess.asFileUri('bootstrap-fork', require).fsPath,
{
serverName: 'File Watcher (nsfw)',
args: ['--type=watcherService'],
env: {
AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/nsfw/watcherApp',
PIPE_LOGGING: 'true',
VERBOSE_LOGGING: 'true' // transmit console logs from server to client
}
}
));
this._register(client.onDidProcessExit(() => {
// our watcher app should never be completed because it keeps on watching. being in here indicates
// that the watcher process died and we want to restart it here. we only do it a max number of times
if (!this.isDisposed) {
if (this.restartCounter <= FileWatcher.MAX_RESTARTS) {
this.error('terminated unexpectedly and is restarted again...');
this.restartCounter++;
this.startWatching();
} else {
this.error('failed to start after retrying for some time, giving up. Please report this as a bug report!');
}
}
}));
// Initialize watcher
this.service = createChannelSender<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
this.service.setVerboseLogging(this.verboseLogging);
this._register(this.service.onDidChangeFile(e => !this.isDisposed && this.onDidFilesChange(e)));
this._register(this.service.onDidLogMessage(m => this.onLogMessage(m)));
// Start watching
this.setFolders(this.folders);
}
setVerboseLogging(verboseLogging: boolean): void {
this.verboseLogging = verboseLogging;
if (!this.isDisposed && this.service) {
this.service.setVerboseLogging(verboseLogging);
}
}
error(message: string) {
this.onLogMessage({ type: 'error', message: `[File Watcher (nsfw)] ${message}` });
}
setFolders(folders: IWatcherRequest[]): void {
this.folders = folders;
if (this.service) {
this.service.setRoots(folders);
}
}
dispose(): void {
this.isDisposed = true;
super.dispose();
}
}

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