mirror of
https://github.com/coder/code-server.git
synced 2026-05-05 20:15:19 +02:00
Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
535
lib/vscode/src/vs/platform/actions/common/actions.ts
Normal file
535
lib/vscode/src/vs/platform/actions/common/actions.ts
Normal 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
|
||||
187
lib/vscode/src/vs/platform/actions/common/menuService.ts
Normal file
187
lib/vscode/src/vs/platform/actions/common/menuService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
48
lib/vscode/src/vs/platform/backup/electron-main/backup.ts
Normal file
48
lib/vscode/src/vs/platform/backup/electron-main/backup.ts
Normal 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>>;
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
22
lib/vscode/src/vs/platform/backup/node/backup.ts
Normal file
22
lib/vscode/src/vs/platform/backup/node/backup.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
105
lib/vscode/src/vs/platform/browser/contextScopedHistoryWidget.ts
Normal file
105
lib/vscode/src/vs/platform/browser/contextScopedHistoryWidget.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
100
lib/vscode/src/vs/platform/clipboard/browser/clipboardService.ts
Normal file
100
lib/vscode/src/vs/platform/clipboard/browser/clipboardService.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
149
lib/vscode/src/vs/platform/commands/common/commands.ts
Normal file
149
lib/vscode/src/vs/platform/commands/common/commands.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
});
|
||||
});
|
||||
372
lib/vscode/src/vs/platform/configuration/common/configuration.ts
Normal file
372
lib/vscode/src/vs/platform/configuration/common/configuration.ts
Normal 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!;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {};
|
||||
}
|
||||
}
|
||||
@@ -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: [] });
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
1145
lib/vscode/src/vs/platform/contextkey/common/contextkey.ts
Normal file
1145
lib/vscode/src/vs/platform/contextkey/common/contextkey.ts
Normal file
File diff suppressed because it is too large
Load Diff
19
lib/vscode/src/vs/platform/contextkey/common/contextkeys.ts
Normal file
19
lib/vscode/src/vs/platform/contextkey/common/contextkeys.ts
Normal 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);
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
107
lib/vscode/src/vs/platform/debug/common/extensionHostDebugIpc.ts
Normal file
107
lib/vscode/src/vs/platform/debug/common/extensionHostDebugIpc.ts
Normal 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]);
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
79
lib/vscode/src/vs/platform/diagnostics/common/diagnostics.ts
Normal file
79
lib/vscode/src/vs/platform/diagnostics/common/diagnostics.ts
Normal 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;
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
307
lib/vscode/src/vs/platform/dialogs/common/dialogs.ts
Normal file
307
lib/vscode/src/vs/platform/dialogs/common/dialogs.ts
Normal 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;
|
||||
}
|
||||
207
lib/vscode/src/vs/platform/dialogs/electron-main/dialogs.ts
Normal file
207
lib/vscode/src/vs/platform/dialogs/electron-main/dialogs.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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(); }
|
||||
}
|
||||
18
lib/vscode/src/vs/platform/download/common/download.ts
Normal file
18
lib/vscode/src/vs/platform/download/common/download.ts
Normal 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>;
|
||||
|
||||
}
|
||||
42
lib/vscode/src/vs/platform/download/common/downloadIpc.ts
Normal file
42
lib/vscode/src/vs/platform/download/common/downloadIpc.ts
Normal 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]);
|
||||
}
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
190
lib/vscode/src/vs/platform/driver/browser/baseDriver.ts
Normal file
190
lib/vscode/src/vs/platform/driver/browser/baseDriver.ts
Normal 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>;
|
||||
}
|
||||
25
lib/vscode/src/vs/platform/driver/browser/driver.ts
Normal file
25
lib/vscode/src/vs/platform/driver/browser/driver.ts
Normal 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;
|
||||
}
|
||||
56
lib/vscode/src/vs/platform/driver/common/driver.ts
Normal file
56
lib/vscode/src/vs/platform/driver/common/driver.ts
Normal 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>;
|
||||
}
|
||||
65
lib/vscode/src/vs/platform/driver/electron-browser/driver.ts
Normal file
65
lib/vscode/src/vs/platform/driver/electron-browser/driver.ts
Normal 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));
|
||||
}
|
||||
224
lib/vscode/src/vs/platform/driver/electron-main/driver.ts
Normal file
224
lib/vscode/src/vs/platform/driver/electron-main/driver.ts
Normal 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);
|
||||
}
|
||||
230
lib/vscode/src/vs/platform/driver/node/driver.ts
Normal file
230
lib/vscode/src/vs/platform/driver/node/driver.ts
Normal 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 };
|
||||
}
|
||||
269
lib/vscode/src/vs/platform/editor/common/editor.ts
Normal file
269
lib/vscode/src/vs/platform/editor/common/editor.ts
Normal 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;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
97
lib/vscode/src/vs/platform/environment/common/argv.ts
Normal file
97
lib/vscode/src/vs/platform/environment/common/argv.ts
Normal 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;
|
||||
}
|
||||
139
lib/vscode/src/vs/platform/environment/common/environment.ts
Normal file
139
lib/vscode/src/vs/platform/environment/common/environment.ts
Normal 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
|
||||
//
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
321
lib/vscode/src/vs/platform/environment/node/argv.ts
Normal file
321
lib/vscode/src/vs/platform/environment/node/argv.ts
Normal 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}`;
|
||||
}
|
||||
|
||||
80
lib/vscode/src/vs/platform/environment/node/argvHelper.ts
Normal file
80
lib/vscode/src/vs/platform/environment/node/argvHelper.ts
Normal 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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
67
lib/vscode/src/vs/platform/environment/node/stdin.ts
Normal file
67
lib/vscode/src/vs/platform/environment/node/stdin.ts
Normal 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());
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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' };
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 |
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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."));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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(() => { }, () => { });
|
||||
}
|
||||
}
|
||||
@@ -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.")));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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/*
|
||||
`;
|
||||
}
|
||||
});
|
||||
@@ -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'));
|
||||
});
|
||||
});
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
307
lib/vscode/src/vs/platform/extensions/common/extensions.ts
Normal file
307
lib/vscode/src/vs/platform/extensions/common/extensions.ts
Normal 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[]>;
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
1204
lib/vscode/src/vs/platform/files/common/fileService.ts
Normal file
1204
lib/vscode/src/vs/platform/files/common/fileService.ts
Normal file
File diff suppressed because it is too large
Load Diff
1003
lib/vscode/src/vs/platform/files/common/files.ts
Normal file
1003
lib/vscode/src/vs/platform/files/common/files.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
134
lib/vscode/src/vs/platform/files/common/io.ts
Normal file
134
lib/vscode/src/vs/platform/files/common/io.ts
Normal 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;
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
702
lib/vscode/src/vs/platform/files/node/diskFileSystemProvider.ts
Normal file
702
lib/vscode/src/vs/platform/files/node/diskFileSystemProvider.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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));
|
||||
@@ -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
Reference in New Issue
Block a user