mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-12 11:08:31 -05:00
Merge vscode 1.67 (#20883)
* Fix initial build breaks from 1.67 merge (#2514) * Update yarn lock files * Update build scripts * Fix tsconfig * Build breaks * WIP * Update yarn lock files * Misc breaks * Updates to package.json * Breaks * Update yarn * Fix breaks * Breaks * Build breaks * Breaks * Breaks * Breaks * Breaks * Breaks * Missing file * Breaks * Breaks * Breaks * Breaks * Breaks * Fix several runtime breaks (#2515) * Missing files * Runtime breaks * Fix proxy ordering issue * Remove commented code * Fix breaks with opening query editor * Fix post merge break * Updates related to setup build and other breaks (#2516) * Fix bundle build issues * Update distro * Fix distro merge and update build JS files * Disable pipeline steps * Remove stats call * Update license name * Make new RPM dependencies a warning * Fix extension manager version checks * Update JS file * Fix a few runtime breaks * Fixes * Fix runtime issues * Fix build breaks * Update notebook tests (part 1) * Fix broken tests * Linting errors * Fix hygiene * Disable lint rules * Bump distro * Turn off smoke tests * Disable integration tests * Remove failing "activate" test * Remove failed test assertion * Disable other broken test * Disable query history tests * Disable extension unit tests * Disable failing tasks
This commit is contained in:
@@ -3,12 +3,14 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { addDisposableListener } from 'vs/base/browser/dom';
|
||||
import { alert } from 'vs/base/browser/ui/aria/aria';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { AccessibilitySupport, CONTEXT_ACCESSIBILITY_MODE_ENABLED, IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
|
||||
|
||||
export class AccessibilityService extends Disposable implements IAccessibilityService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
@@ -17,21 +19,62 @@ export class AccessibilityService extends Disposable implements IAccessibilitySe
|
||||
protected _accessibilitySupport = AccessibilitySupport.Unknown;
|
||||
protected readonly _onDidChangeScreenReaderOptimized = new Emitter<void>();
|
||||
|
||||
protected _configMotionReduced: 'auto' | 'on' | 'off';
|
||||
protected _systemMotionReduced: boolean;
|
||||
protected readonly _onDidChangeReducedMotion = new Emitter<void>();
|
||||
|
||||
constructor(
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
|
||||
@ILayoutService private readonly _layoutService: ILayoutService,
|
||||
@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();
|
||||
}
|
||||
if (e.affectsConfiguration('workbench.reduceMotion')) {
|
||||
this._configMotionReduced = this._configurationService.getValue('workbench.reduceMotion');
|
||||
this._onDidChangeReducedMotion.fire();
|
||||
}
|
||||
}));
|
||||
updateContextKey();
|
||||
this.onDidChangeScreenReaderOptimized(() => updateContextKey());
|
||||
this._register(this.onDidChangeScreenReaderOptimized(() => updateContextKey()));
|
||||
|
||||
const reduceMotionMatcher = window.matchMedia(`(prefers-reduced-motion: reduce)`);
|
||||
this._systemMotionReduced = reduceMotionMatcher.matches;
|
||||
this._configMotionReduced = this._configurationService.getValue<'auto' | 'on' | 'off'>('workbench.reduceMotion');
|
||||
|
||||
this.initReducedMotionListeners(reduceMotionMatcher);
|
||||
}
|
||||
|
||||
private initReducedMotionListeners(reduceMotionMatcher: MediaQueryList) {
|
||||
|
||||
if (!this._layoutService.hasContainer) {
|
||||
// we can't use `ILayoutService.container` because the application
|
||||
// doesn't have a single container
|
||||
return;
|
||||
}
|
||||
|
||||
this._register(addDisposableListener(reduceMotionMatcher, 'change', () => {
|
||||
this._systemMotionReduced = reduceMotionMatcher.matches;
|
||||
if (this._configMotionReduced === 'auto') {
|
||||
this._onDidChangeReducedMotion.fire();
|
||||
}
|
||||
}));
|
||||
|
||||
const updateRootClasses = () => {
|
||||
const reduce = this.isMotionReduced();
|
||||
this._layoutService.container.classList.toggle('reduce-motion', reduce);
|
||||
this._layoutService.container.classList.toggle('enable-motion', !reduce);
|
||||
};
|
||||
|
||||
updateRootClasses();
|
||||
this._register(this.onDidChangeReducedMotion(() => updateRootClasses()));
|
||||
}
|
||||
|
||||
get onDidChangeScreenReaderOptimized(): Event<void> {
|
||||
@@ -43,14 +86,23 @@ export class AccessibilityService extends Disposable implements IAccessibilitySe
|
||||
return config === 'on' || (config === 'auto' && this._accessibilitySupport === AccessibilitySupport.Enabled);
|
||||
}
|
||||
|
||||
getAccessibilitySupport(): AccessibilitySupport {
|
||||
return this._accessibilitySupport;
|
||||
get onDidChangeReducedMotion(): Event<void> {
|
||||
return this._onDidChangeReducedMotion.event;
|
||||
}
|
||||
|
||||
isMotionReduced(): boolean {
|
||||
const config = this._configMotionReduced;
|
||||
return config === 'on' || (config === 'auto' && this._systemMotionReduced);
|
||||
}
|
||||
|
||||
alwaysUnderlineAccessKeys(): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
getAccessibilitySupport(): AccessibilitySupport {
|
||||
return this._accessibilitySupport;
|
||||
}
|
||||
|
||||
setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void {
|
||||
if (this._accessibilitySupport === accessibilitySupport) {
|
||||
return;
|
||||
|
||||
@@ -13,9 +13,11 @@ export interface IAccessibilityService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly onDidChangeScreenReaderOptimized: Event<void>;
|
||||
readonly onDidChangeReducedMotion: Event<void>;
|
||||
|
||||
alwaysUnderlineAccessKeys(): Promise<boolean>;
|
||||
isScreenReaderOptimized(): boolean;
|
||||
isMotionReduced(): boolean;
|
||||
getAccessibilitySupport(): AccessibilitySupport;
|
||||
setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void;
|
||||
alert(message: string): void;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility';
|
||||
|
||||
export class TestAccessibilityService implements IAccessibilityService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
onDidChangeScreenReaderOptimized = Event.None;
|
||||
onDidChangeReducedMotion = Event.None;
|
||||
|
||||
isScreenReaderOptimized(): boolean { return false; }
|
||||
isMotionReduced(): boolean { return false; }
|
||||
alwaysUnderlineAccessKeys(): Promise<boolean> { return Promise.resolve(false); }
|
||||
setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void { }
|
||||
getAccessibilitySupport(): AccessibilitySupport { return AccessibilitySupport.Unknown; }
|
||||
alert(message: string): void { }
|
||||
}
|
||||
46
src/vs/platform/action/common/action.ts
Normal file
46
src/vs/platform/action/common/action.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { UriDto } from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
export interface ILocalizedString {
|
||||
|
||||
/**
|
||||
* The localized value of the string.
|
||||
*/
|
||||
value: string;
|
||||
|
||||
/**
|
||||
* The original (non localized value of the string)
|
||||
*/
|
||||
original: string;
|
||||
}
|
||||
|
||||
export interface ICommandActionTitle extends ILocalizedString {
|
||||
|
||||
/**
|
||||
* The title with a mnemonic designation. && precedes the mnemonic.
|
||||
*/
|
||||
mnemonicTitle?: string;
|
||||
}
|
||||
|
||||
export type Icon = { dark?: URI; light?: URI } | ThemeIcon;
|
||||
|
||||
export interface ICommandAction {
|
||||
id: string;
|
||||
title: string | ICommandActionTitle;
|
||||
shortTitle?: string | ICommandActionTitle;
|
||||
category?: string | ILocalizedString;
|
||||
tooltip?: string | ILocalizedString;
|
||||
icon?: Icon;
|
||||
source?: string;
|
||||
precondition?: ContextKeyExpression;
|
||||
toggled?: ContextKeyExpression | { condition: ContextKeyExpression; icon?: Icon; tooltip?: string; title?: string | ILocalizedString };
|
||||
}
|
||||
|
||||
export type ISerializableCommandAction = UriDto<ICommandAction>;
|
||||
@@ -11,7 +11,8 @@
|
||||
background-size: 16px;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item.menu-entry .action-label {
|
||||
.monaco-action-bar .action-item.menu-entry .action-label,
|
||||
.hc-light .monaco-action-bar .action-item.menu-entry .action-label {
|
||||
background-image: var(--menu-entry-icon-light);
|
||||
}
|
||||
|
||||
@@ -39,7 +40,8 @@
|
||||
background-size: 16px;
|
||||
}
|
||||
|
||||
.monaco-dropdown-with-default > .action-container.menu-entry > .action-label {
|
||||
.monaco-dropdown-with-default > .action-container.menu-entry > .action-label,
|
||||
.hc-light .monaco-dropdown-with-default > .action-container.menu-entry > .action-label {
|
||||
background-image: var(--menu-entry-icon-light);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ import { DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'v
|
||||
import { isLinux, isWindows, OS } from 'vs/base/common/platform';
|
||||
import 'vs/css!./menuEntryActionViewItem';
|
||||
import { localize } from 'vs/nls';
|
||||
import { ICommandAction, Icon, IMenu, IMenuActionOptions, IMenuService, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions';
|
||||
import { IMenu, IMenuActionOptions, IMenuService, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions';
|
||||
import { ICommandAction, Icon } from 'vs/platform/action/common/action';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
@@ -24,7 +25,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
|
||||
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, primaryGroup?: string): IDisposable {
|
||||
export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[] }, primaryGroup?: string): IDisposable {
|
||||
const groups = menu.getActions(options);
|
||||
const modifierKeyEmitter = ModifierKeyEmitter.getInstance();
|
||||
const useAlternativeActions = modifierKeyEmitter.keyStatus.altKey || ((isWindows || isLinux) && modifierKeyEmitter.keyStatus.shiftKey);
|
||||
@@ -32,7 +33,7 @@ export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuAct
|
||||
return asDisposable(groups);
|
||||
}
|
||||
|
||||
export function createAndFillInActionBarActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, primaryGroup?: string | ((actionGroup: string) => boolean), primaryMaxCount?: number, shouldInlineSubmenu?: (action: SubmenuAction, group: string, groupSize: number) => boolean, useSeparatorsInPrimaryActions?: boolean): IDisposable {
|
||||
export function createAndFillInActionBarActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[] }, primaryGroup?: string | ((actionGroup: string) => boolean), primaryMaxCount?: number, shouldInlineSubmenu?: (action: SubmenuAction, group: string, groupSize: number) => boolean, useSeparatorsInPrimaryActions?: boolean): IDisposable {
|
||||
const groups = menu.getActions(options);
|
||||
const isPrimaryAction = typeof primaryGroup === 'string' ? (actionGroup: string) => actionGroup === primaryGroup : primaryGroup;
|
||||
|
||||
@@ -52,7 +53,7 @@ function asDisposable(groups: ReadonlyArray<[string, ReadonlyArray<MenuItemActio
|
||||
}
|
||||
|
||||
export function fillInActions( // {{SQL CARBON EDIT}} add export modifier
|
||||
groups: ReadonlyArray<[string, ReadonlyArray<MenuItemAction | SubmenuItemAction>]>, target: IAction[] | { primary: IAction[]; secondary: IAction[]; },
|
||||
groups: ReadonlyArray<[string, ReadonlyArray<MenuItemAction | SubmenuItemAction>]>, target: IAction[] | { primary: IAction[]; secondary: IAction[] },
|
||||
useAlternativeActions: boolean,
|
||||
isPrimaryAction: (actionGroup: string) => boolean = actionGroup => actionGroup === 'navigation',
|
||||
primaryMaxCount: number = Number.MAX_SAFE_INTEGER,
|
||||
@@ -70,7 +71,7 @@ export function fillInActions( // {{SQL CARBON EDIT}} add export modifier
|
||||
secondaryBucket = target.secondary;
|
||||
}
|
||||
|
||||
const submenuInfo = new Set<{ group: string, action: SubmenuAction, index: number }>();
|
||||
const submenuInfo = new Set<{ group: string; action: SubmenuAction; index: number }>();
|
||||
|
||||
for (const [group, actions] of groups) {
|
||||
|
||||
@@ -121,6 +122,7 @@ export function fillInActions( // {{SQL CARBON EDIT}} add export modifier
|
||||
|
||||
export interface IMenuEntryActionViewItemOptions {
|
||||
draggable?: boolean;
|
||||
keybinding?: string;
|
||||
}
|
||||
|
||||
export class MenuEntryActionViewItem extends ActionViewItem {
|
||||
@@ -136,7 +138,7 @@ export class MenuEntryActionViewItem extends ActionViewItem {
|
||||
@INotificationService protected _notificationService: INotificationService,
|
||||
@IContextKeyService protected _contextKeyService: IContextKeyService
|
||||
) {
|
||||
super(undefined, _action, { icon: !!(_action.class || _action.item.icon), label: !_action.class && !_action.item.icon, draggable: options?.draggable });
|
||||
super(undefined, _action, { icon: !!(_action.class || _action.item.icon), label: !_action.class && !_action.item.icon, draggable: options?.draggable, keybinding: options?.keybinding });
|
||||
this._altKey = ModifierKeyEmitter.getInstance();
|
||||
}
|
||||
|
||||
@@ -170,7 +172,7 @@ export class MenuEntryActionViewItem extends ActionViewItem {
|
||||
let alternativeKeyDown = this._altKey.keyStatus.altKey || ((isWindows || isLinux) && this._altKey.keyStatus.shiftKey);
|
||||
|
||||
const updateAltState = () => {
|
||||
const wantsAltCommand = mouseOver && alternativeKeyDown;
|
||||
const wantsAltCommand = mouseOver && alternativeKeyDown && !!this._commandAction.alt?.enabled;
|
||||
if (wantsAltCommand !== this._wantsAltCommand) {
|
||||
this._wantsAltCommand = wantsAltCommand;
|
||||
this.updateLabel();
|
||||
@@ -212,7 +214,7 @@ export class MenuEntryActionViewItem extends ActionViewItem {
|
||||
let title = keybindingLabel
|
||||
? localize('titleAndKb', "{0} ({1})", tooltip, keybindingLabel)
|
||||
: tooltip;
|
||||
if (!this._wantsAltCommand && this._menuItemAction.alt) {
|
||||
if (!this._wantsAltCommand && this._menuItemAction.alt?.enabled) {
|
||||
const altTooltip = this._menuItemAction.alt.tooltip || this._menuItemAction.alt.label;
|
||||
const altKeybinding = this._keybindingService.lookupKeybinding(this._menuItemAction.alt.id, this._contextKeyService);
|
||||
const altKeybindingLabel = altKeybinding && altKeybinding.getLabel();
|
||||
@@ -222,6 +224,7 @@ export class MenuEntryActionViewItem extends ActionViewItem {
|
||||
title += `\n[${UILabelProvider.modifierLabels[OS].altKey}] ${altTitleSection}`;
|
||||
}
|
||||
this.label.title = title;
|
||||
this.label.setAttribute('aria-label', title);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +313,12 @@ export class SubmenuEntryActionViewItem extends DropdownMenuActionViewItem {
|
||||
}
|
||||
}
|
||||
|
||||
class DropdownWithDefaultActionViewItem extends BaseActionViewItem {
|
||||
export interface IDropdownWithDefaultActionViewItemOptions extends IDropdownMenuActionViewItemOptions {
|
||||
renderKeybindingWithDefaultActionLabel?: boolean;
|
||||
}
|
||||
|
||||
export class DropdownWithDefaultActionViewItem extends BaseActionViewItem {
|
||||
private readonly _options: IDropdownWithDefaultActionViewItemOptions | undefined;
|
||||
private _defaultAction: ActionViewItem;
|
||||
private _dropdown: DropdownMenuActionViewItem;
|
||||
private _container: HTMLElement | null = null;
|
||||
@@ -322,7 +330,7 @@ class DropdownWithDefaultActionViewItem extends BaseActionViewItem {
|
||||
|
||||
constructor(
|
||||
submenuAction: SubmenuItemAction,
|
||||
options: IDropdownMenuActionViewItemOptions | undefined,
|
||||
options: IDropdownWithDefaultActionViewItemOptions | undefined,
|
||||
@IKeybindingService protected readonly _keybindingService: IKeybindingService,
|
||||
@INotificationService protected _notificationService: INotificationService,
|
||||
@IContextMenuService protected _contextMenuService: IContextMenuService,
|
||||
@@ -331,7 +339,7 @@ class DropdownWithDefaultActionViewItem extends BaseActionViewItem {
|
||||
@IStorageService protected _storageService: IStorageService
|
||||
) {
|
||||
super(null, submenuAction);
|
||||
|
||||
this._options = options;
|
||||
this._storageKey = `${submenuAction.item.submenu._debugName}_lastActionId`;
|
||||
|
||||
// determine default action
|
||||
@@ -344,7 +352,7 @@ class DropdownWithDefaultActionViewItem extends BaseActionViewItem {
|
||||
defaultAction = submenuAction.actions[0];
|
||||
}
|
||||
|
||||
this._defaultAction = this._instaService.createInstance(MenuEntryActionViewItem, <MenuItemAction>defaultAction, undefined);
|
||||
this._defaultAction = this._instaService.createInstance(MenuEntryActionViewItem, <MenuItemAction>defaultAction, { keybinding: this._getDefaultActionKeybindingLabel(defaultAction) });
|
||||
|
||||
const dropdownOptions = Object.assign({}, options ?? Object.create(null), {
|
||||
menuAsChild: options?.menuAsChild ?? true,
|
||||
@@ -364,7 +372,7 @@ class DropdownWithDefaultActionViewItem extends BaseActionViewItem {
|
||||
this._storageService.store(this._storageKey, lastAction.id, StorageScope.WORKSPACE, StorageTarget.USER);
|
||||
|
||||
this._defaultAction.dispose();
|
||||
this._defaultAction = this._instaService.createInstance(MenuEntryActionViewItem, lastAction, undefined);
|
||||
this._defaultAction = this._instaService.createInstance(MenuEntryActionViewItem, lastAction, { keybinding: this._getDefaultActionKeybindingLabel(lastAction) });
|
||||
this._defaultAction.actionRunner = new class extends ActionRunner {
|
||||
override async runAction(action: IAction, context?: unknown): Promise<void> {
|
||||
await action.run(undefined);
|
||||
@@ -376,6 +384,17 @@ class DropdownWithDefaultActionViewItem extends BaseActionViewItem {
|
||||
}
|
||||
}
|
||||
|
||||
private _getDefaultActionKeybindingLabel(defaultAction: IAction) {
|
||||
let defaultActionKeybinding: string | undefined;
|
||||
if (this._options?.renderKeybindingWithDefaultActionLabel) {
|
||||
const kb = this._keybindingService.lookupKeybinding(defaultAction.id);
|
||||
if (kb) {
|
||||
defaultActionKeybinding = `(${kb.getLabel()})`;
|
||||
}
|
||||
}
|
||||
return defaultActionKeybinding;
|
||||
}
|
||||
|
||||
override setActionContext(newContext: unknown): void {
|
||||
super.setActionContext(newContext);
|
||||
this._defaultAction.setActionContext(newContext);
|
||||
|
||||
@@ -9,49 +9,14 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { LinkedList } from 'vs/base/common/linkedList';
|
||||
import { UriDto } from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ICommandAction, ICommandActionTitle, Icon, ILocalizedString } from 'vs/platform/action/common/action';
|
||||
import { CommandsRegistry, ICommandHandlerDescription, ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { SyncDescriptor, SyncDescriptor0 } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { BrandedService, createDecorator, IConstructorSignature2, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { BrandedService, createDecorator, IConstructorSignature, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IKeybindingRule, IKeybindings, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
export interface ILocalizedString {
|
||||
/**
|
||||
* The localized value of the string.
|
||||
*/
|
||||
value: string;
|
||||
/**
|
||||
* The original (non localized value of the string)
|
||||
*/
|
||||
original: string;
|
||||
}
|
||||
|
||||
export interface ICommandActionTitle extends ILocalizedString {
|
||||
/**
|
||||
* The title with a mnemonic designation. && precedes the mnemonic.
|
||||
*/
|
||||
mnemonicTitle?: string;
|
||||
}
|
||||
|
||||
export type Icon = { dark?: URI; light?: URI; } | ThemeIcon;
|
||||
|
||||
export interface ICommandAction {
|
||||
id: string;
|
||||
title: string | ICommandActionTitle;
|
||||
shortTitle?: string | ICommandActionTitle;
|
||||
category?: string | ILocalizedString;
|
||||
tooltip?: string | ILocalizedString;
|
||||
icon?: Icon;
|
||||
source?: string;
|
||||
precondition?: ContextKeyExpression;
|
||||
toggled?: ContextKeyExpression | { condition: ContextKeyExpression, icon?: Icon, tooltip?: string, title?: string | ILocalizedString };
|
||||
}
|
||||
|
||||
export type ISerializableCommandAction = UriDto<ICommandAction>;
|
||||
|
||||
export interface IMenuItem {
|
||||
command: ICommandAction;
|
||||
alt?: ICommandAction;
|
||||
@@ -89,6 +54,7 @@ export class MenuId {
|
||||
static readonly DebugVariablesContext = new MenuId('DebugVariablesContext');
|
||||
static readonly DebugWatchContext = new MenuId('DebugWatchContext');
|
||||
static readonly DebugToolBar = new MenuId('DebugToolBar');
|
||||
static readonly DebugToolBarStop = new MenuId('DebugToolBarStop');
|
||||
static readonly EditorContext = new MenuId('EditorContext');
|
||||
static readonly SimpleEditorContext = new MenuId('SimpleEditorContext');
|
||||
static readonly EditorContextCopy = new MenuId('EditorContextCopy');
|
||||
@@ -101,6 +67,10 @@ export class MenuId {
|
||||
static readonly ExplorerContext = new MenuId('ExplorerContext');
|
||||
static readonly ExtensionContext = new MenuId('ExtensionContext');
|
||||
static readonly GlobalActivity = new MenuId('GlobalActivity');
|
||||
static readonly TitleMenu = new MenuId('TitleMenu');
|
||||
static readonly TitleMenuQuickPick = new MenuId('TitleMenuQuickPick');
|
||||
static readonly LayoutControlMenuSubmenu = new MenuId('LayoutControlMenuSubmenu');
|
||||
static readonly LayoutControlMenu = new MenuId('LayoutControlMenu');
|
||||
static readonly MenubarMainMenu = new MenuId('MenubarMainMenu');
|
||||
static readonly MenubarAppearanceMenu = new MenuId('MenubarAppearanceMenu');
|
||||
static readonly MenubarDebugMenu = new MenuId('MenubarDebugMenu');
|
||||
@@ -111,6 +81,8 @@ export class MenuId {
|
||||
static readonly MenubarHelpMenu = new MenuId('MenubarHelpMenu');
|
||||
static readonly MenubarLayoutMenu = new MenuId('MenubarLayoutMenu');
|
||||
static readonly MenubarNewBreakpointMenu = new MenuId('MenubarNewBreakpointMenu');
|
||||
static readonly MenubarPanelAlignmentMenu = new MenuId('MenubarPanelAlignmentMenu');
|
||||
static readonly MenubarPanelPositionMenu = new MenuId('MenubarPanelPositionMenu');
|
||||
static readonly MenubarPreferencesMenu = new MenuId('MenubarPreferencesMenu');
|
||||
static readonly MenubarRecentMenu = new MenuId('MenubarRecentMenu');
|
||||
static readonly MenubarSelectionMenu = new MenuId('MenubarSelectionMenu');
|
||||
@@ -182,8 +154,10 @@ export class MenuId {
|
||||
static readonly TimelineItemContext = new MenuId('TimelineItemContext');
|
||||
static readonly TimelineTitle = new MenuId('TimelineTitle');
|
||||
static readonly TimelineTitleContext = new MenuId('TimelineTitleContext');
|
||||
static readonly TimelineFilterSubMenu = new MenuId('TimelineFilterSubMenu');
|
||||
static readonly AccountsContext = new MenuId('AccountsContext');
|
||||
static readonly PanelTitle = new MenuId('PanelTitle');
|
||||
static readonly AuxiliaryBarTitle = new MenuId('AuxiliaryBarTitle');
|
||||
static readonly TerminalInstanceContext = new MenuId('TerminalInstanceContext');
|
||||
static readonly TerminalEditorInstanceContext = new MenuId('TerminalEditorInstanceContext');
|
||||
static readonly TerminalNewDropdownContext = new MenuId('TerminalNewDropdownContext');
|
||||
@@ -240,7 +214,7 @@ export interface IMenuRegistry {
|
||||
addCommand(userCommand: ICommandAction): IDisposable;
|
||||
getCommand(id: string): ICommandAction | undefined;
|
||||
getCommands(): ICommandsMap;
|
||||
appendMenuItems(items: Iterable<{ id: MenuId, item: IMenuItem | ISubmenuItem }>): IDisposable;
|
||||
appendMenuItems(items: Iterable<{ id: MenuId; item: IMenuItem | ISubmenuItem }>): IDisposable;
|
||||
appendMenuItem(menu: MenuId, item: IMenuItem | ISubmenuItem): IDisposable;
|
||||
getMenuItems(loc: MenuId): Array<IMenuItem | ISubmenuItem>;
|
||||
}
|
||||
@@ -291,7 +265,7 @@ export const MenuRegistry: IMenuRegistry = new class implements IMenuRegistry {
|
||||
return this.appendMenuItems(Iterable.single({ id, item }));
|
||||
}
|
||||
|
||||
appendMenuItems(items: Iterable<{ id: MenuId, item: IMenuItem | ISubmenuItem }>): IDisposable {
|
||||
appendMenuItems(items: Iterable<{ id: MenuId; item: IMenuItem | ISubmenuItem }>): IDisposable {
|
||||
|
||||
const changedIds = new Set<MenuId>();
|
||||
const toRemove = new LinkedList<Function>();
|
||||
@@ -353,21 +327,6 @@ export const MenuRegistry: IMenuRegistry = new class implements IMenuRegistry {
|
||||
}
|
||||
};
|
||||
|
||||
export class ExecuteCommandAction extends Action {
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@ICommandService private readonly _commandService: ICommandService) {
|
||||
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
override run(...args: any[]): Promise<void> {
|
||||
return this._commandService.executeCommand(this.id, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
export class SubmenuItemAction extends SubmenuAction {
|
||||
|
||||
constructor(
|
||||
@@ -432,7 +391,7 @@ export class MenuItemAction implements IAction {
|
||||
|
||||
if (item.toggled) {
|
||||
const toggled = ((item.toggled as { condition: ContextKeyExpression }).condition ? item.toggled : { condition: item.toggled }) as {
|
||||
condition: ContextKeyExpression, icon?: Icon, tooltip?: string | ILocalizedString, title?: string | ILocalizedString
|
||||
condition: ContextKeyExpression; icon?: Icon; tooltip?: string | ILocalizedString; title?: string | ILocalizedString;
|
||||
};
|
||||
this.checked = contextKeyService.contextMatchesRules(toggled.condition);
|
||||
if (this.checked && toggled.tooltip) {
|
||||
@@ -486,7 +445,7 @@ export class SyncActionDescriptor {
|
||||
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);
|
||||
return new SyncActionDescriptor(ctor as IConstructorSignature<Action, [string, string | undefined]>, id, label, keybindings, keybindingContext, keybindingWeight);
|
||||
}
|
||||
|
||||
public static from<Services extends BrandedService[]>(
|
||||
@@ -500,7 +459,7 @@ export class SyncActionDescriptor {
|
||||
return SyncActionDescriptor.create(ctor, ctor.ID, ctor.LABEL, keybindings, keybindingContext, keybindingWeight);
|
||||
}
|
||||
|
||||
private constructor(ctor: IConstructorSignature2<string, string | undefined, Action>,
|
||||
private constructor(ctor: IConstructorSignature<Action, [string, string | undefined]>,
|
||||
id: string, label: string | undefined, keybindings?: IKeybindings, keybindingContext?: ContextKeyExpression, keybindingWeight?: number
|
||||
) {
|
||||
this._id = id;
|
||||
@@ -508,7 +467,7 @@ export class SyncActionDescriptor {
|
||||
this._keybindings = keybindings;
|
||||
this._keybindingContext = keybindingContext;
|
||||
this._keybindingWeight = keybindingWeight;
|
||||
this._descriptor = new SyncDescriptor(ctor, [this._id, this._label]) as unknown as SyncDescriptor0<Action>;
|
||||
this._descriptor = new SyncDescriptor(ctor, [this._id, this._label]);
|
||||
}
|
||||
|
||||
public get syncDescriptor(): SyncDescriptor0<Action> {
|
||||
@@ -613,4 +572,21 @@ export function registerAction2(ctor: { new(): Action2 }): IDisposable {
|
||||
|
||||
return disposables;
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}} - add this class back since it was removed upstream
|
||||
export class ExecuteCommandAction extends Action {
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@ICommandService private readonly _commandService: ICommandService) {
|
||||
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
override run(...args: any[]): Promise<void> {
|
||||
return this._commandService.executeCommand(this.id, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { ILocalizedString, IMenu, IMenuActionOptions, IMenuCreateOptions, IMenuItem, IMenuService, isIMenuItem, ISubmenuItem, MenuId, MenuItemAction, MenuRegistry, SubmenuItemAction } from 'vs/platform/actions/common/actions';
|
||||
import { IMenu, IMenuActionOptions, IMenuCreateOptions, IMenuItem, IMenuService, isIMenuItem, ISubmenuItem, MenuId, MenuItemAction, MenuRegistry, SubmenuItemAction } from 'vs/platform/actions/common/actions';
|
||||
import { ILocalizedString } from 'vs/platform/action/common/action';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
|
||||
|
||||
116
src/vs/platform/assignment/common/assignment.ts
Normal file
116
src/vs/platform/assignment/common/assignment.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { IExperimentationFilterProvider } from 'tas-client-umd';
|
||||
|
||||
export const ASSIGNMENT_STORAGE_KEY = 'VSCode.ABExp.FeatureData';
|
||||
export const ASSIGNMENT_REFETCH_INTERVAL = 0; // no polling
|
||||
|
||||
export interface IAssignmentService {
|
||||
readonly _serviceBrand: undefined;
|
||||
getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined>;
|
||||
}
|
||||
|
||||
export enum TargetPopulation {
|
||||
Team = 'team',
|
||||
Internal = 'internal',
|
||||
Insiders = 'insider',
|
||||
Public = 'public',
|
||||
}
|
||||
|
||||
/*
|
||||
Based upon the official VSCode currently existing filters in the
|
||||
ExP backend for the VSCode cluster.
|
||||
https://experimentation.visualstudio.com/Analysis%20and%20Experimentation/_git/AnE.ExP.TAS.TachyonHost.Configuration?path=%2FConfigurations%2Fvscode%2Fvscode.json&version=GBmaster
|
||||
"X-MSEdge-Market": "detection.market",
|
||||
"X-FD-Corpnet": "detection.corpnet",
|
||||
"X-VSCode-AppVersion": "appversion",
|
||||
"X-VSCode-Build": "build",
|
||||
"X-MSEdge-ClientId": "clientid",
|
||||
"X-VSCode-ExtensionName": "extensionname",
|
||||
"X-VSCode-TargetPopulation": "targetpopulation",
|
||||
"X-VSCode-Language": "language"
|
||||
*/
|
||||
export enum Filters {
|
||||
/**
|
||||
* The market in which the extension is distributed.
|
||||
*/
|
||||
Market = 'X-MSEdge-Market',
|
||||
|
||||
/**
|
||||
* The corporation network.
|
||||
*/
|
||||
CorpNet = 'X-FD-Corpnet',
|
||||
|
||||
/**
|
||||
* Version of the application which uses experimentation service.
|
||||
*/
|
||||
ApplicationVersion = 'X-VSCode-AppVersion',
|
||||
|
||||
/**
|
||||
* Insiders vs Stable.
|
||||
*/
|
||||
Build = 'X-VSCode-Build',
|
||||
|
||||
/**
|
||||
* Client Id which is used as primary unit for the experimentation.
|
||||
*/
|
||||
ClientId = 'X-MSEdge-ClientId',
|
||||
|
||||
/**
|
||||
* Extension header.
|
||||
*/
|
||||
ExtensionName = 'X-VSCode-ExtensionName',
|
||||
|
||||
/**
|
||||
* The language in use by VS Code
|
||||
*/
|
||||
Language = 'X-VSCode-Language',
|
||||
|
||||
/**
|
||||
* The target population.
|
||||
* This is used to separate internal, early preview, GA, etc.
|
||||
*/
|
||||
TargetPopulation = 'X-VSCode-TargetPopulation',
|
||||
}
|
||||
|
||||
export class AssignmentFilterProvider implements IExperimentationFilterProvider {
|
||||
constructor(
|
||||
private version: string,
|
||||
private appName: string,
|
||||
private machineId: string,
|
||||
private targetPopulation: TargetPopulation
|
||||
) { }
|
||||
|
||||
getFilterValue(filter: string): string | null {
|
||||
switch (filter) {
|
||||
case Filters.ApplicationVersion:
|
||||
return this.version; // productService.version
|
||||
case Filters.Build:
|
||||
return this.appName; // productService.nameLong
|
||||
case Filters.ClientId:
|
||||
return this.machineId;
|
||||
case Filters.Language:
|
||||
return platform.language;
|
||||
case Filters.ExtensionName:
|
||||
return 'vscode-core'; // always return vscode-core for exp service
|
||||
case Filters.TargetPopulation:
|
||||
return this.targetPopulation;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
getFilters(): Map<string, any> {
|
||||
let filters: Map<string, any> = new Map<string, any>();
|
||||
let filterValues = Object.values(Filters);
|
||||
for (let value of filterValues) {
|
||||
filters.set(value, this.getFilterValue(value));
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
}
|
||||
123
src/vs/platform/assignment/common/assignmentService.ts
Normal file
123
src/vs/platform/assignment/common/assignmentService.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type { IExperimentationTelemetry, ExperimentationService as TASClient, IKeyValueStorage } from 'tas-client-umd';
|
||||
import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { getTelemetryLevel } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { AssignmentFilterProvider, ASSIGNMENT_REFETCH_INTERVAL, ASSIGNMENT_STORAGE_KEY, IAssignmentService, TargetPopulation } from 'vs/platform/assignment/common/assignment';
|
||||
|
||||
class NullAssignmentServiceTelemetry implements IExperimentationTelemetry {
|
||||
constructor() { }
|
||||
|
||||
setSharedProperty(name: string, value: string): void {
|
||||
// noop due to lack of telemetry service
|
||||
}
|
||||
|
||||
postEvent(eventName: string, props: Map<string, string>): void {
|
||||
// noop due to lack of telemetry service
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class BaseAssignmentService implements IAssignmentService {
|
||||
_serviceBrand: undefined;
|
||||
protected tasClient: Promise<TASClient> | undefined;
|
||||
private networkInitialized = false;
|
||||
private overrideInitDelay: Promise<void>;
|
||||
|
||||
protected get experimentsEnabled(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly getMachineId: () => Promise<string>,
|
||||
protected readonly configurationService: IConfigurationService,
|
||||
protected readonly productService: IProductService,
|
||||
protected telemetry: IExperimentationTelemetry,
|
||||
private keyValueStorage?: IKeyValueStorage
|
||||
) {
|
||||
|
||||
if (productService.tasConfig && this.experimentsEnabled && getTelemetryLevel(this.configurationService) === TelemetryLevel.USAGE) {
|
||||
this.tasClient = this.setupTASClient();
|
||||
}
|
||||
|
||||
// For development purposes, configure the delay until tas local tas treatment ovverrides are available
|
||||
const overrideDelaySetting = this.configurationService.getValue('experiments.overrideDelay');
|
||||
const overrideDelay = typeof overrideDelaySetting === 'number' ? overrideDelaySetting : 0;
|
||||
this.overrideInitDelay = new Promise(resolve => setTimeout(resolve, overrideDelay));
|
||||
}
|
||||
|
||||
async getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined> {
|
||||
// For development purposes, allow overriding tas assignments to test variants locally.
|
||||
await this.overrideInitDelay;
|
||||
const override = this.configurationService.getValue<T>('experiments.override.' + name);
|
||||
if (override !== undefined) {
|
||||
return override;
|
||||
}
|
||||
|
||||
if (!this.tasClient) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!this.experimentsEnabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let result: T | undefined;
|
||||
const client = await this.tasClient;
|
||||
|
||||
// The TAS client is initialized but we need to check if the initial fetch has completed yet
|
||||
// If it is complete, return a cached value for the treatment
|
||||
// If not, use the async call with `checkCache: true`. This will allow the module to return a cached value if it is present.
|
||||
// Otherwise it will await the initial fetch to return the most up to date value.
|
||||
if (this.networkInitialized) {
|
||||
result = client.getTreatmentVariable<T>('vscode', name);
|
||||
} else {
|
||||
result = await client.getTreatmentVariableAsync<T>('vscode', name, true);
|
||||
}
|
||||
|
||||
result = client.getTreatmentVariable<T>('vscode', name);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async setupTASClient(): Promise<TASClient> {
|
||||
const targetPopulation = this.productService.quality === 'stable' ? TargetPopulation.Public : TargetPopulation.Insiders;
|
||||
const machineId = await this.getMachineId();
|
||||
const filterProvider = new AssignmentFilterProvider(
|
||||
this.productService.version,
|
||||
this.productService.nameLong,
|
||||
machineId,
|
||||
targetPopulation
|
||||
);
|
||||
|
||||
const tasConfig = this.productService.tasConfig!;
|
||||
const tasClient = new (await import('tas-client-umd')).ExperimentationService({
|
||||
filterProviders: [filterProvider],
|
||||
telemetry: this.telemetry,
|
||||
storageKey: ASSIGNMENT_STORAGE_KEY,
|
||||
keyValueStorage: this.keyValueStorage,
|
||||
featuresTelemetryPropertyName: tasConfig.featuresTelemetryPropertyName,
|
||||
assignmentContextTelemetryPropertyName: tasConfig.assignmentContextTelemetryPropertyName,
|
||||
telemetryEventName: tasConfig.telemetryEventName,
|
||||
endpoint: tasConfig.endpoint,
|
||||
refetchInterval: ASSIGNMENT_REFETCH_INTERVAL,
|
||||
});
|
||||
|
||||
await tasClient.initializePromise;
|
||||
tasClient.initialFetch.then(() => this.networkInitialized = true);
|
||||
|
||||
return tasClient;
|
||||
}
|
||||
}
|
||||
|
||||
export class AssignmentService extends BaseAssignmentService {
|
||||
constructor(
|
||||
machineId: string,
|
||||
configurationService: IConfigurationService,
|
||||
productService: IProductService) {
|
||||
super(() => Promise.resolve(machineId), configurationService, productService, new NullAssignmentServiceTelemetry());
|
||||
}
|
||||
}
|
||||
25
src/vs/platform/backup/common/backup.ts
Normal file
25
src/vs/platform/backup/common/backup.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
|
||||
|
||||
export interface IWorkspaceBackupInfo {
|
||||
readonly workspace: IWorkspaceIdentifier;
|
||||
readonly remoteAuthority?: string;
|
||||
}
|
||||
|
||||
export interface IFolderBackupInfo {
|
||||
readonly folderUri: URI;
|
||||
readonly remoteAuthority?: string;
|
||||
}
|
||||
|
||||
export function isFolderBackupInfo(curr: IWorkspaceBackupInfo | IFolderBackupInfo): curr is IFolderBackupInfo {
|
||||
return curr && curr.hasOwnProperty('folderUri');
|
||||
}
|
||||
|
||||
export function isWorkspaceBackupInfo(curr: IWorkspaceBackupInfo | IFolderBackupInfo): curr is IWorkspaceBackupInfo {
|
||||
return curr && curr.hasOwnProperty('workspace');
|
||||
}
|
||||
@@ -6,32 +6,22 @@
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { IFolderBackupInfo, IWorkspaceBackupInfo } from 'vs/platform/backup/common/backup';
|
||||
import { IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
|
||||
|
||||
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[];
|
||||
getFolderBackupPaths(): IFolderBackupInfo[];
|
||||
getEmptyWindowBackupPaths(): IEmptyWindowBackupInfo[];
|
||||
|
||||
registerWorkspaceBackupSync(workspace: IWorkspaceBackupInfo, migrateFrom?: string): string;
|
||||
registerFolderBackupSync(folderUri: URI): string;
|
||||
registerFolderBackupSync(folderUri: IFolderBackupInfo): string;
|
||||
registerEmptyWindowBackupSync(backupFolder?: string, remoteAuthority?: string): string;
|
||||
|
||||
unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void;
|
||||
@@ -44,5 +34,5 @@ export interface IBackupMainService {
|
||||
* it checks for each backup location if any backups
|
||||
* are stored.
|
||||
*/
|
||||
getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>>;
|
||||
getDirtyWorkspaces(): Promise<Array<IWorkspaceBackupInfo | IFolderBackupInfo>>;
|
||||
}
|
||||
|
||||
@@ -12,13 +12,14 @@ import { isLinux } from 'vs/base/common/platform';
|
||||
import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Promises, RimRafMode, writeFileSync } from 'vs/base/node/pfs';
|
||||
import { IBackupMainService, isWorkspaceBackupInfo, IWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup';
|
||||
import { IBackupWorkspacesFormat, IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
|
||||
import { IBackupMainService } from 'vs/platform/backup/electron-main/backup';
|
||||
import { IBackupWorkspacesFormat, IDeprecatedBackupWorkspacesFormat, IEmptyWindowBackupInfo, isEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
|
||||
import { HotExitConfiguration, IFilesConfiguration } from 'vs/platform/files/common/files';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { IFolderBackupInfo, isFolderBackupInfo, IWorkspaceBackupInfo } from 'vs/platform/backup/common/backup';
|
||||
import { IWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
|
||||
|
||||
export class BackupMainService implements IBackupMainService {
|
||||
|
||||
@@ -28,7 +29,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
protected workspacesJsonPath: string;
|
||||
|
||||
private workspaces: IWorkspaceBackupInfo[] = [];
|
||||
private folders: URI[] = [];
|
||||
private folders: IFolderBackupInfo[] = [];
|
||||
private emptyWindows: IEmptyWindowBackupInfo[] = [];
|
||||
|
||||
// Comparers for paths and resources that will
|
||||
@@ -47,7 +48,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
let backups: IBackupWorkspacesFormat;
|
||||
let backups: IBackupWorkspacesFormat & IDeprecatedBackupWorkspacesFormat;
|
||||
try {
|
||||
backups = JSON.parse(await Promises.readFile(this.workspacesJsonPath, 'utf8')); // invalid JSON or permission issue can happen here
|
||||
} catch (error) {
|
||||
@@ -74,10 +75,12 @@ export class BackupMainService implements IBackupMainService {
|
||||
this.workspaces = await this.validateWorkspaces(rootWorkspaces);
|
||||
|
||||
// read folder backups
|
||||
let workspaceFolders: URI[] = [];
|
||||
let workspaceFolders: IFolderBackupInfo[] = [];
|
||||
try {
|
||||
if (Array.isArray(backups.folderURIWorkspaces)) {
|
||||
workspaceFolders = backups.folderURIWorkspaces.map(folder => URI.parse(folder));
|
||||
if (Array.isArray(backups.folderWorkspaceInfos)) {
|
||||
workspaceFolders = backups.folderWorkspaceInfos.map(folder => ({ folderUri: URI.parse(folder.folderUri), remoteAuthority: folder.remoteAuthority }));
|
||||
} else if (Array.isArray(backups.folderURIWorkspaces)) {
|
||||
workspaceFolders = backups.folderURIWorkspaces.map(folder => ({ folderUri: URI.parse(folder), remoteAuthority: undefined }));
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore URI parsing exceptions
|
||||
@@ -100,7 +103,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
return this.workspaces.slice(0); // return a copy
|
||||
}
|
||||
|
||||
getFolderBackupPaths(): URI[] {
|
||||
getFolderBackupPaths(): IFolderBackupInfo[] {
|
||||
if (this.isHotExitOnExitAndWindowClose()) {
|
||||
// Only non-folder windows are restored on main process launch when
|
||||
// hot exit is configured as onExitAndWindowClose.
|
||||
@@ -169,17 +172,17 @@ export class BackupMainService implements IBackupMainService {
|
||||
}
|
||||
}
|
||||
|
||||
registerFolderBackupSync(folderUri: URI): string {
|
||||
if (!this.folders.some(folder => this.backupUriComparer.isEqual(folderUri, folder))) {
|
||||
this.folders.push(folderUri);
|
||||
registerFolderBackupSync(folderInfo: IFolderBackupInfo): string {
|
||||
if (!this.folders.some(folder => this.backupUriComparer.isEqual(folderInfo.folderUri, folder.folderUri))) {
|
||||
this.folders.push(folderInfo);
|
||||
this.saveSync();
|
||||
}
|
||||
|
||||
return this.getBackupPath(this.getFolderHash(folderUri));
|
||||
return this.getBackupPath(this.getFolderHash(folderInfo));
|
||||
}
|
||||
|
||||
unregisterFolderBackupSync(folderUri: URI): void {
|
||||
const index = this.folders.findIndex(folder => this.backupUriComparer.isEqual(folderUri, folder));
|
||||
const index = this.folders.findIndex(folder => this.backupUriComparer.isEqual(folderUri, folder.folderUri));
|
||||
if (index !== -1) {
|
||||
this.folders.splice(index, 1);
|
||||
this.saveSync();
|
||||
@@ -248,25 +251,26 @@ export class BackupMainService implements IBackupMainService {
|
||||
return result;
|
||||
}
|
||||
|
||||
private async validateFolders(folderWorkspaces: URI[]): Promise<URI[]> {
|
||||
private async validateFolders(folderWorkspaces: IFolderBackupInfo[]): Promise<IFolderBackupInfo[]> {
|
||||
if (!Array.isArray(folderWorkspaces)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: URI[] = [];
|
||||
const result: IFolderBackupInfo[] = [];
|
||||
const seenIds: Set<string> = new Set();
|
||||
for (let folderURI of folderWorkspaces) {
|
||||
for (let folderInfo of folderWorkspaces) {
|
||||
const folderURI = folderInfo.folderUri;
|
||||
const key = this.backupUriComparer.getComparisonKey(folderURI);
|
||||
if (!seenIds.has(key)) {
|
||||
seenIds.add(key);
|
||||
|
||||
const backupPath = this.getBackupPath(this.getFolderHash(folderURI));
|
||||
const backupPath = this.getBackupPath(this.getFolderHash(folderInfo));
|
||||
const hasBackups = await this.doHasBackups(backupPath);
|
||||
|
||||
// If the folder has no backups, ignore it
|
||||
if (hasBackups) {
|
||||
if (folderURI.scheme !== Schemas.file || await Promises.exists(folderURI.fsPath)) {
|
||||
result.push(folderURI);
|
||||
result.push(folderInfo);
|
||||
} else {
|
||||
// If the folder has backups, but the target workspace is missing, convert backups to empty ones
|
||||
await this.convertToEmptyWindowBackup(backupPath);
|
||||
@@ -362,13 +366,13 @@ export class BackupMainService implements IBackupMainService {
|
||||
return true;
|
||||
}
|
||||
|
||||
async getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>> {
|
||||
const dirtyWorkspaces: Array<IWorkspaceIdentifier | URI> = [];
|
||||
async getDirtyWorkspaces(): Promise<Array<IWorkspaceBackupInfo | IFolderBackupInfo>> {
|
||||
const dirtyWorkspaces: Array<IWorkspaceBackupInfo | IFolderBackupInfo> = [];
|
||||
|
||||
// Workspaces with backups
|
||||
for (const workspace of this.workspaces) {
|
||||
if ((await this.hasBackups(workspace))) {
|
||||
dirtyWorkspaces.push(workspace.workspace);
|
||||
dirtyWorkspaces.push(workspace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,22 +386,22 @@ export class BackupMainService implements IBackupMainService {
|
||||
return dirtyWorkspaces;
|
||||
}
|
||||
|
||||
private hasBackups(backupLocation: IWorkspaceBackupInfo | IEmptyWindowBackupInfo | URI): Promise<boolean> {
|
||||
private hasBackups(backupLocation: IWorkspaceBackupInfo | IEmptyWindowBackupInfo | IFolderBackupInfo): Promise<boolean> {
|
||||
let backupPath: string;
|
||||
|
||||
// Empty
|
||||
if (isEmptyWindowBackupInfo(backupLocation)) {
|
||||
backupPath = backupLocation.backupFolder;
|
||||
}
|
||||
|
||||
// Folder
|
||||
if (URI.isUri(backupLocation)) {
|
||||
else if (isFolderBackupInfo(backupLocation)) {
|
||||
backupPath = this.getBackupPath(this.getFolderHash(backupLocation));
|
||||
}
|
||||
|
||||
// Workspace
|
||||
else if (isWorkspaceBackupInfo(backupLocation)) {
|
||||
backupPath = this.getBackupPath(backupLocation.workspace.id);
|
||||
}
|
||||
|
||||
// Empty
|
||||
else {
|
||||
backupPath = backupLocation.backupFolder;
|
||||
backupPath = this.getBackupPath(backupLocation.workspace.id);
|
||||
}
|
||||
|
||||
return this.doHasBackups(backupPath);
|
||||
@@ -443,7 +447,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
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()),
|
||||
folderWorkspaceInfos: this.folders.map(folder => ({ folderUri: folder.folderUri.toString(), remoteAuthority: folder.remoteAuthority })),
|
||||
emptyWorkspaceInfos: this.emptyWindows
|
||||
};
|
||||
}
|
||||
@@ -452,7 +456,8 @@ export class BackupMainService implements IBackupMainService {
|
||||
return (Date.now() + Math.round(Math.random() * 1000)).toString();
|
||||
}
|
||||
|
||||
protected getFolderHash(folderUri: URI): string {
|
||||
protected getFolderHash(folder: IFolderBackupInfo): string {
|
||||
const folderUri = folder.folderUri;
|
||||
let key: string;
|
||||
|
||||
if (folderUri.scheme === Schemas.file) {
|
||||
|
||||
@@ -3,15 +3,27 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface ISerializedWorkspace { id: string; configURIPath: string; remoteAuthority?: string; }
|
||||
export interface ISerializedWorkspace { id: string; configURIPath: string; remoteAuthority?: string }
|
||||
|
||||
export interface ISerializedFolder { folderUri: string; remoteAuthority?: string }
|
||||
|
||||
export interface IBackupWorkspacesFormat {
|
||||
rootURIWorkspaces: ISerializedWorkspace[];
|
||||
folderURIWorkspaces: string[];
|
||||
folderWorkspaceInfos: ISerializedFolder[];
|
||||
emptyWorkspaceInfos: IEmptyWindowBackupInfo[];
|
||||
}
|
||||
|
||||
/** Deprecated since 1.64 */
|
||||
export interface IDeprecatedBackupWorkspacesFormat {
|
||||
folderURIWorkspaces: string[]; // replaced by folderWorkspaceInfos
|
||||
}
|
||||
|
||||
export interface IEmptyWindowBackupInfo {
|
||||
backupFolder: string;
|
||||
remoteAuthority?: string;
|
||||
}
|
||||
|
||||
export function isEmptyWindowBackupInfo(obj: unknown): obj is IEmptyWindowBackupInfo {
|
||||
const candidate = obj as IEmptyWindowBackupInfo;
|
||||
return typeof candidate?.backupFolder === 'string';
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import { isEqual } from 'vs/base/common/resources';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
|
||||
import { IWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup';
|
||||
import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainService';
|
||||
import { IBackupWorkspacesFormat, ISerializedWorkspace } from 'vs/platform/backup/node/backup';
|
||||
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||
@@ -23,12 +22,14 @@ import { OPTIONS, parseArgs } from 'vs/platform/environment/node/argv';
|
||||
import { HotExitConfiguration } from 'vs/platform/files/common/files';
|
||||
import { ConsoleMainLogger, LogService } from 'vs/platform/log/common/log';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { IFolderBackupInfo, isFolderBackupInfo, IWorkspaceBackupInfo } from 'vs/platform/backup/common/backup';
|
||||
import { IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
|
||||
|
||||
flakySuite('BackupMainService', () => {
|
||||
|
||||
function assertEqualUris(actual: URI[], expected: URI[]) {
|
||||
assert.deepStrictEqual(actual.map(a => a.toString()), expected.map(a => a.toString()));
|
||||
function assertEqualFolderInfos(actual: IFolderBackupInfo[], expected: IFolderBackupInfo[]) {
|
||||
const withUriAsString = (f: IFolderBackupInfo) => ({ folderUri: f.folderUri.toString(), remoteAuthority: f.remoteAuthority });
|
||||
assert.deepStrictEqual(actual.map(withUriAsString), expected.map(withUriAsString));
|
||||
}
|
||||
|
||||
function toWorkspace(path: string): IWorkspaceIdentifier {
|
||||
@@ -48,6 +49,10 @@ flakySuite('BackupMainService', () => {
|
||||
};
|
||||
}
|
||||
|
||||
function toFolderBackupInfo(uri: URI, remoteAuthority?: string): IFolderBackupInfo {
|
||||
return { folderUri: uri, remoteAuthority };
|
||||
}
|
||||
|
||||
function toSerializedWorkspace(ws: IWorkspaceIdentifier): ISerializedWorkspace {
|
||||
return {
|
||||
id: ws.id,
|
||||
@@ -90,7 +95,7 @@ flakySuite('BackupMainService', () => {
|
||||
const fooFile = URI.file(platform.isWindows ? 'C:\\foo' : '/foo');
|
||||
const barFile = URI.file(platform.isWindows ? 'C:\\bar' : '/bar');
|
||||
|
||||
let service: BackupMainService & { toBackupPath(arg: URI | string): string, getFolderHash(folderUri: URI): string };
|
||||
let service: BackupMainService & { toBackupPath(arg: URI | string): string; getFolderHash(folder: IFolderBackupInfo): string };
|
||||
let configService: TestConfigurationService;
|
||||
|
||||
let environmentService: EnvironmentMainService;
|
||||
@@ -119,12 +124,12 @@ flakySuite('BackupMainService', () => {
|
||||
}
|
||||
|
||||
toBackupPath(arg: URI | string): string {
|
||||
const id = arg instanceof URI ? super.getFolderHash(arg) : arg;
|
||||
const id = arg instanceof URI ? super.getFolderHash({ folderUri: arg }) : arg;
|
||||
return path.join(this.backupHome, id);
|
||||
}
|
||||
|
||||
override getFolderHash(folderUri: URI): string {
|
||||
return super.getFolderHash(folderUri);
|
||||
override getFolderHash(folder: IFolderBackupInfo): string {
|
||||
return super.getFolderHash(folder);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -138,18 +143,18 @@ flakySuite('BackupMainService', () => {
|
||||
test('service validates backup workspaces on startup and cleans up (folder workspaces)', async function () {
|
||||
|
||||
// 1) backup workspace path does not exist
|
||||
service.registerFolderBackupSync(fooFile);
|
||||
service.registerFolderBackupSync(barFile);
|
||||
service.registerFolderBackupSync(toFolderBackupInfo(fooFile));
|
||||
service.registerFolderBackupSync(toFolderBackupInfo(barFile));
|
||||
await service.initialize();
|
||||
assertEqualUris(service.getFolderBackupPaths(), []);
|
||||
assertEqualFolderInfos(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);
|
||||
service.registerFolderBackupSync(toFolderBackupInfo(fooFile));
|
||||
service.registerFolderBackupSync(toFolderBackupInfo(barFile));
|
||||
await service.initialize();
|
||||
assertEqualUris(service.getFolderBackupPaths(), []);
|
||||
assertEqualFolderInfos(service.getFolderBackupPaths(), []);
|
||||
assert.ok(!fs.existsSync(service.toBackupPath(fooFile)));
|
||||
assert.ok(!fs.existsSync(service.toBackupPath(barFile)));
|
||||
|
||||
@@ -158,10 +163,10 @@ flakySuite('BackupMainService', () => {
|
||||
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);
|
||||
service.registerFolderBackupSync(toFolderBackupInfo(fooFile));
|
||||
service.registerFolderBackupSync(toFolderBackupInfo(barFile));
|
||||
await service.initialize();
|
||||
assertEqualUris(service.getFolderBackupPaths(), []);
|
||||
assertEqualFolderInfos(service.getFolderBackupPaths(), []);
|
||||
assert.ok(!fs.existsSync(service.toBackupPath(fooFile)));
|
||||
assert.ok(!fs.existsSync(service.toBackupPath(barFile)));
|
||||
|
||||
@@ -171,7 +176,7 @@ flakySuite('BackupMainService', () => {
|
||||
fs.mkdirSync(service.toBackupPath(fooFile));
|
||||
fs.mkdirSync(service.toBackupPath(barFile));
|
||||
fs.mkdirSync(fileBackups);
|
||||
service.registerFolderBackupSync(fooFile);
|
||||
service.registerFolderBackupSync(toFolderBackupInfo(fooFile));
|
||||
assert.strictEqual(service.getFolderBackupPaths().length, 1);
|
||||
assert.strictEqual(service.getEmptyWindowBackupPaths().length, 0);
|
||||
fs.writeFileSync(path.join(fileBackups, 'backup.txt'), '');
|
||||
@@ -229,7 +234,7 @@ flakySuite('BackupMainService', () => {
|
||||
const backupPathToMigrate = service.toBackupPath(fooFile);
|
||||
fs.mkdirSync(backupPathToMigrate);
|
||||
fs.writeFileSync(path.join(backupPathToMigrate, 'backup.txt'), 'Some Data');
|
||||
service.registerFolderBackupSync(URI.file(backupPathToMigrate));
|
||||
service.registerFolderBackupSync(toFolderBackupInfo(URI.file(backupPathToMigrate)));
|
||||
|
||||
const workspaceBackupPath = service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(barFile.fsPath), backupPathToMigrate);
|
||||
|
||||
@@ -245,12 +250,12 @@ flakySuite('BackupMainService', () => {
|
||||
const backupPathToMigrate = service.toBackupPath(fooFile);
|
||||
fs.mkdirSync(backupPathToMigrate);
|
||||
fs.writeFileSync(path.join(backupPathToMigrate, 'backup.txt'), 'Some Data');
|
||||
service.registerFolderBackupSync(URI.file(backupPathToMigrate));
|
||||
service.registerFolderBackupSync(toFolderBackupInfo(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));
|
||||
service.registerFolderBackupSync(toFolderBackupInfo(URI.file(backupPathToPreserve)));
|
||||
|
||||
const workspaceBackupPath = service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(barFile.fsPath), backupPathToMigrate);
|
||||
|
||||
@@ -265,54 +270,63 @@ flakySuite('BackupMainService', () => {
|
||||
|
||||
suite('loadSync', () => {
|
||||
test('getFolderBackupPaths() should return [] when workspaces.json doesn\'t exist', () => {
|
||||
assertEqualUris(service.getFolderBackupPaths(), []);
|
||||
assertEqualFolderInfos(service.getFolderBackupPaths(), []);
|
||||
});
|
||||
|
||||
test('getFolderBackupPaths() should return [] when workspaces.json is not properly formed JSON', async () => {
|
||||
fs.writeFileSync(backupWorkspacesPath, '');
|
||||
await service.initialize();
|
||||
assertEqualUris(service.getFolderBackupPaths(), []);
|
||||
assertEqualFolderInfos(service.getFolderBackupPaths(), []);
|
||||
fs.writeFileSync(backupWorkspacesPath, '{]');
|
||||
await service.initialize();
|
||||
assertEqualUris(service.getFolderBackupPaths(), []);
|
||||
assertEqualFolderInfos(service.getFolderBackupPaths(), []);
|
||||
fs.writeFileSync(backupWorkspacesPath, 'foo');
|
||||
await service.initialize();
|
||||
assertEqualUris(service.getFolderBackupPaths(), []);
|
||||
assertEqualFolderInfos(service.getFolderBackupPaths(), []);
|
||||
});
|
||||
|
||||
test('getFolderBackupPaths() should return [] when folderWorkspaces in workspaces.json is absent', async () => {
|
||||
test('getFolderBackupPaths() should return [] when folderWorkspaceInfos in workspaces.json is absent', async () => {
|
||||
fs.writeFileSync(backupWorkspacesPath, '{}');
|
||||
await service.initialize();
|
||||
assertEqualUris(service.getFolderBackupPaths(), []);
|
||||
assertEqualFolderInfos(service.getFolderBackupPaths(), []);
|
||||
});
|
||||
|
||||
test('getFolderBackupPaths() should return [] when folderWorkspaces in workspaces.json is not a string array', async () => {
|
||||
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{}}');
|
||||
test('getFolderBackupPaths() should return [] when folderWorkspaceInfos in workspaces.json is not a string array', async () => {
|
||||
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaceInfos":{}}');
|
||||
await service.initialize();
|
||||
assertEqualUris(service.getFolderBackupPaths(), []);
|
||||
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": ["bar"]}}');
|
||||
assertEqualFolderInfos(service.getFolderBackupPaths(), []);
|
||||
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaceInfos":{"foo": ["bar"]}}');
|
||||
await service.initialize();
|
||||
assertEqualUris(service.getFolderBackupPaths(), []);
|
||||
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": []}}');
|
||||
assertEqualFolderInfos(service.getFolderBackupPaths(), []);
|
||||
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaceInfos":{"foo": []}}');
|
||||
await service.initialize();
|
||||
assertEqualUris(service.getFolderBackupPaths(), []);
|
||||
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": "bar"}}');
|
||||
assertEqualFolderInfos(service.getFolderBackupPaths(), []);
|
||||
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaceInfos":{"foo": "bar"}}');
|
||||
await service.initialize();
|
||||
assertEqualUris(service.getFolderBackupPaths(), []);
|
||||
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":"foo"}');
|
||||
assertEqualFolderInfos(service.getFolderBackupPaths(), []);
|
||||
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaceInfos":"foo"}');
|
||||
await service.initialize();
|
||||
assertEqualUris(service.getFolderBackupPaths(), []);
|
||||
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":1}');
|
||||
assertEqualFolderInfos(service.getFolderBackupPaths(), []);
|
||||
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaceInfos":1}');
|
||||
await service.initialize();
|
||||
assertEqualUris(service.getFolderBackupPaths(), []);
|
||||
assertEqualFolderInfos(service.getFolderBackupPaths(), []);
|
||||
});
|
||||
|
||||
test('getFolderBackupPaths() should migrate folderURIWorkspaces', async () => {
|
||||
await ensureFolderExists(existingTestFolder1);
|
||||
|
||||
fs.writeFileSync(backupWorkspacesPath, JSON.stringify({ folderURIWorkspaces: [existingTestFolder1.toString()] }));
|
||||
await service.initialize();
|
||||
assertEqualFolderInfos(service.getFolderBackupPaths(), [toFolderBackupInfo(existingTestFolder1)]);
|
||||
});
|
||||
|
||||
test('getFolderBackupPaths() should return [] when files.hotExit = "onExitAndWindowClose"', async () => {
|
||||
service.registerFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase()));
|
||||
assertEqualUris(service.getFolderBackupPaths(), [URI.file(fooFile.fsPath.toUpperCase())]);
|
||||
const fi = toFolderBackupInfo(URI.file(fooFile.fsPath.toUpperCase()));
|
||||
service.registerFolderBackupSync(fi);
|
||||
assertEqualFolderInfos(service.getFolderBackupPaths(), [fi]);
|
||||
configService.setUserConfiguration('files.hotExit', HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE);
|
||||
await service.initialize();
|
||||
assertEqualUris(service.getFolderBackupPaths(), []);
|
||||
assertEqualFolderInfos(service.getFolderBackupPaths(), []);
|
||||
});
|
||||
|
||||
test('getWorkspaceBackups() should return [] when workspaces.json doesn\'t exist', () => {
|
||||
@@ -383,7 +397,7 @@ flakySuite('BackupMainService', () => {
|
||||
const upperFooPath = fooFile.fsPath.toUpperCase();
|
||||
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(upperFooPath));
|
||||
assert.strictEqual(service.getWorkspaceBackups().length, 1);
|
||||
assertEqualUris(service.getWorkspaceBackups().map(r => r.workspace.configPath), [URI.file(upperFooPath)]);
|
||||
assert.deepStrictEqual(service.getWorkspaceBackups().map(r => r.workspace.configPath.toString()), [URI.file(upperFooPath).toString()]);
|
||||
configService.setUserConfiguration('files.hotExit', HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE);
|
||||
await service.initialize();
|
||||
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
|
||||
@@ -440,7 +454,7 @@ flakySuite('BackupMainService', () => {
|
||||
|
||||
const workspacesJson: IBackupWorkspacesFormat = {
|
||||
rootURIWorkspaces: [],
|
||||
folderURIWorkspaces: [existingTestFolder1.toString(), existingTestFolder1.toString()],
|
||||
folderWorkspaceInfos: [{ folderUri: existingTestFolder1.toString() }, { folderUri: existingTestFolder1.toString() }],
|
||||
emptyWorkspaceInfos: []
|
||||
};
|
||||
await pfs.Promises.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
|
||||
@@ -448,7 +462,7 @@ flakySuite('BackupMainService', () => {
|
||||
|
||||
const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
|
||||
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
|
||||
assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
|
||||
assert.deepStrictEqual(json.folderWorkspaceInfos, [{ folderUri: existingTestFolder1.toString() }]);
|
||||
});
|
||||
|
||||
test('should ignore duplicates on Windows and Mac (folder workspace)', async () => {
|
||||
@@ -457,14 +471,14 @@ flakySuite('BackupMainService', () => {
|
||||
|
||||
const workspacesJson: IBackupWorkspacesFormat = {
|
||||
rootURIWorkspaces: [],
|
||||
folderURIWorkspaces: [existingTestFolder1.toString(), existingTestFolder1.toString().toLowerCase()],
|
||||
folderWorkspaceInfos: [{ folderUri: existingTestFolder1.toString() }, { folderUri: existingTestFolder1.toString().toLowerCase() }],
|
||||
emptyWorkspaceInfos: []
|
||||
};
|
||||
await pfs.Promises.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
|
||||
await service.initialize();
|
||||
const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
|
||||
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
|
||||
assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
|
||||
assert.deepStrictEqual(json.folderWorkspaceInfos, [{ folderUri: existingTestFolder1.toString() }]);
|
||||
});
|
||||
|
||||
test('should ignore duplicates on Windows and Mac (root workspace)', async () => {
|
||||
@@ -478,7 +492,7 @@ flakySuite('BackupMainService', () => {
|
||||
|
||||
const workspacesJson: IBackupWorkspacesFormat = {
|
||||
rootURIWorkspaces: [workspace1, workspace2, workspace3].map(toSerializedWorkspace),
|
||||
folderURIWorkspaces: [],
|
||||
folderWorkspaceInfos: [],
|
||||
emptyWorkspaceInfos: []
|
||||
};
|
||||
await pfs.Promises.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
|
||||
@@ -497,12 +511,12 @@ flakySuite('BackupMainService', () => {
|
||||
|
||||
suite('registerWindowForBackups', () => {
|
||||
test('should persist paths to workspaces.json (folder workspace)', async () => {
|
||||
service.registerFolderBackupSync(fooFile);
|
||||
service.registerFolderBackupSync(barFile);
|
||||
assertEqualUris(service.getFolderBackupPaths(), [fooFile, barFile]);
|
||||
service.registerFolderBackupSync(toFolderBackupInfo(fooFile));
|
||||
service.registerFolderBackupSync(toFolderBackupInfo(barFile));
|
||||
assertEqualFolderInfos(service.getFolderBackupPaths(), [toFolderBackupInfo(fooFile), toFolderBackupInfo(barFile)]);
|
||||
const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
|
||||
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
|
||||
assert.deepStrictEqual(json.folderURIWorkspaces, [fooFile.toString(), barFile.toString()]);
|
||||
assert.deepStrictEqual(json.folderWorkspaceInfos, [{ folderUri: fooFile.toString() }, { folderUri: barFile.toString() }]);
|
||||
});
|
||||
|
||||
test('should persist paths to workspaces.json (root workspace)', async () => {
|
||||
@@ -511,7 +525,7 @@ flakySuite('BackupMainService', () => {
|
||||
const ws2 = toWorkspaceBackupInfo(barFile.fsPath);
|
||||
service.registerWorkspaceBackupSync(ws2);
|
||||
|
||||
assertEqualUris(service.getWorkspaceBackups().map(b => b.workspace.configPath), [fooFile, barFile]);
|
||||
assert.deepStrictEqual(service.getWorkspaceBackups().map(b => b.workspace.configPath.toString()), [fooFile.toString(), barFile.toString()]);
|
||||
assert.strictEqual(ws1.workspace.id, service.getWorkspaceBackups()[0].workspace.id);
|
||||
assert.strictEqual(ws2.workspace.id, service.getWorkspaceBackups()[1].workspace.id);
|
||||
|
||||
@@ -525,18 +539,18 @@ flakySuite('BackupMainService', () => {
|
||||
});
|
||||
|
||||
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())]);
|
||||
service.registerFolderBackupSync(toFolderBackupInfo(URI.file(fooFile.fsPath.toUpperCase())));
|
||||
assertEqualFolderInfos(service.getFolderBackupPaths(), [toFolderBackupInfo(URI.file(fooFile.fsPath.toUpperCase()))]);
|
||||
|
||||
const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
|
||||
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
|
||||
assert.deepStrictEqual(json.folderURIWorkspaces, [URI.file(fooFile.fsPath.toUpperCase()).toString()]);
|
||||
assert.deepStrictEqual(json.folderWorkspaceInfos, [{ folderUri: 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)]);
|
||||
assert.deepStrictEqual(service.getWorkspaceBackups().map(b => b.workspace.configPath.toString()), [URI.file(upperFooPath).toString()]);
|
||||
|
||||
const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
|
||||
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
|
||||
@@ -545,18 +559,18 @@ flakySuite('BackupMainService', () => {
|
||||
|
||||
suite('removeBackupPathSync', () => {
|
||||
test('should remove folder workspaces from workspaces.json (folder workspace)', async () => {
|
||||
service.registerFolderBackupSync(fooFile);
|
||||
service.registerFolderBackupSync(barFile);
|
||||
service.registerFolderBackupSync(toFolderBackupInfo(fooFile));
|
||||
service.registerFolderBackupSync(toFolderBackupInfo(barFile));
|
||||
service.unregisterFolderBackupSync(fooFile);
|
||||
|
||||
const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
|
||||
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
|
||||
assert.deepStrictEqual(json.folderURIWorkspaces, [barFile.toString()]);
|
||||
assert.deepStrictEqual(json.folderWorkspaceInfos, [{ folderUri: barFile.toString() }]);
|
||||
service.unregisterFolderBackupSync(barFile);
|
||||
|
||||
const content = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
|
||||
const json2 = (<IBackupWorkspacesFormat>JSON.parse(content));
|
||||
assert.deepStrictEqual(json2.folderURIWorkspaces, []);
|
||||
assert.deepStrictEqual(json2.folderWorkspaceInfos, []);
|
||||
});
|
||||
|
||||
test('should remove folder workspaces from workspaces.json (root workspace)', async () => {
|
||||
@@ -595,33 +609,37 @@ flakySuite('BackupMainService', () => {
|
||||
|
||||
await ensureFolderExists(existingTestFolder1); // make sure backup folder exists, so the folder is not removed on loadSync
|
||||
|
||||
const workspacesJson: IBackupWorkspacesFormat = { rootURIWorkspaces: [], folderURIWorkspaces: [existingTestFolder1.toString()], emptyWorkspaceInfos: [] };
|
||||
const workspacesJson: IBackupWorkspacesFormat = { rootURIWorkspaces: [], folderWorkspaceInfos: [{ folderUri: existingTestFolder1.toString() }], emptyWorkspaceInfos: [] };
|
||||
await pfs.Promises.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
|
||||
await service.initialize();
|
||||
service.unregisterFolderBackupSync(barFile);
|
||||
service.unregisterEmptyWindowBackupSync('test');
|
||||
const content = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
|
||||
const json = (<IBackupWorkspacesFormat>JSON.parse(content));
|
||||
assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
|
||||
assert.deepStrictEqual(json.folderWorkspaceInfos, [{ folderUri: existingTestFolder1.toString() }]);
|
||||
});
|
||||
});
|
||||
|
||||
suite('getWorkspaceHash', () => {
|
||||
(platform.isLinux ? test.skip : test)('should ignore case on Windows and Mac', () => {
|
||||
const assertFolderHash = (uri1: URI, uri2: URI) => {
|
||||
assert.strictEqual(service.getFolderHash(toFolderBackupInfo(uri1)), service.getFolderHash(toFolderBackupInfo(uri2)));
|
||||
};
|
||||
|
||||
if (platform.isMacintosh) {
|
||||
assert.strictEqual(service.getFolderHash(URI.file('/foo')), service.getFolderHash(URI.file('/FOO')));
|
||||
assertFolderHash(URI.file('/foo'), URI.file('/FOO'));
|
||||
}
|
||||
|
||||
if (platform.isWindows) {
|
||||
assert.strictEqual(service.getFolderHash(URI.file('c:\\foo')), service.getFolderHash(URI.file('C:\\FOO')));
|
||||
assertFolderHash(URI.file('c:\\foo'), 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()));
|
||||
service.registerFolderBackupSync(toFolderBackupInfo(fooFile));
|
||||
service.registerFolderBackupSync(toFolderBackupInfo(URI.file(fooFile.fsPath.toUpperCase())));
|
||||
|
||||
if (platform.isLinux) {
|
||||
assert.strictEqual(service.getFolderBackupPaths().length, 2);
|
||||
@@ -644,12 +662,12 @@ flakySuite('BackupMainService', () => {
|
||||
test('should handle case insensitive paths properly (removeBackupPathSync) (folder workspace)', () => {
|
||||
|
||||
// same case
|
||||
service.registerFolderBackupSync(fooFile);
|
||||
service.registerFolderBackupSync(toFolderBackupInfo(fooFile));
|
||||
service.unregisterFolderBackupSync(fooFile);
|
||||
assert.strictEqual(service.getFolderBackupPaths().length, 0);
|
||||
|
||||
// mixed case
|
||||
service.registerFolderBackupSync(fooFile);
|
||||
service.registerFolderBackupSync(toFolderBackupInfo(fooFile));
|
||||
service.unregisterFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase()));
|
||||
|
||||
if (platform.isLinux) {
|
||||
@@ -662,7 +680,7 @@ flakySuite('BackupMainService', () => {
|
||||
|
||||
suite('getDirtyWorkspaces', () => {
|
||||
test('should report if a workspace or folder has backups', async () => {
|
||||
const folderBackupPath = service.registerFolderBackupSync(fooFile);
|
||||
const folderBackupPath = service.registerFolderBackupSync(toFolderBackupInfo(fooFile));
|
||||
|
||||
const backupWorkspaceInfo = toWorkspaceBackupInfo(fooFile.fsPath);
|
||||
const workspaceBackupPath = service.registerWorkspaceBackupSync(backupWorkspaceInfo);
|
||||
@@ -686,12 +704,12 @@ flakySuite('BackupMainService', () => {
|
||||
|
||||
let found = 0;
|
||||
for (const dirtyWorkpspace of dirtyWorkspaces) {
|
||||
if (URI.isUri(dirtyWorkpspace)) {
|
||||
if (isEqual(fooFile, dirtyWorkpspace)) {
|
||||
if (isFolderBackupInfo(dirtyWorkpspace)) {
|
||||
if (isEqual(fooFile, dirtyWorkpspace.folderUri)) {
|
||||
found++;
|
||||
}
|
||||
} else {
|
||||
if (isEqual(backupWorkspaceInfo.workspace.configPath, dirtyWorkpspace.configPath)) {
|
||||
if (isEqual(backupWorkspaceInfo.workspace.configPath, dirtyWorkpspace.workspace.configPath)) {
|
||||
found++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { $ } from 'vs/base/browser/dom';
|
||||
import { isSafari, isWebkitWebView } from 'vs/base/browser/browser';
|
||||
import { $, addDisposableListener } from 'vs/base/browser/dom';
|
||||
import { DeferredPromise } from 'vs/base/common/async';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ClipboardData, IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; // {{SQL CARBON EDIT}}
|
||||
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
export class BrowserClipboardService implements IClipboardService {
|
||||
export class BrowserClipboardService extends Disposable implements IClipboardService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
@@ -18,6 +23,58 @@ export class BrowserClipboardService implements IClipboardService {
|
||||
throw new Error('Not Implemented');
|
||||
}
|
||||
|
||||
constructor(
|
||||
@ILayoutService private readonly layoutService: ILayoutService,
|
||||
@ILogService private readonly logService: ILogService) {
|
||||
super();
|
||||
if (isSafari || isWebkitWebView) {
|
||||
this.installWebKitWriteTextWorkaround();
|
||||
}
|
||||
}
|
||||
|
||||
private webKitPendingClipboardWritePromise: DeferredPromise<string> | undefined;
|
||||
|
||||
// In Safari, it has the following note:
|
||||
//
|
||||
// "The request to write to the clipboard must be triggered during a user gesture.
|
||||
// A call to clipboard.write or clipboard.writeText outside the scope of a user
|
||||
// gesture(such as "click" or "touch" event handlers) will result in the immediate
|
||||
// rejection of the promise returned by the API call."
|
||||
// From: https://webkit.org/blog/10855/async-clipboard-api/
|
||||
//
|
||||
// Since extensions run in a web worker, and handle gestures in an asynchronous way,
|
||||
// they are not classified by Safari as "in response to a user gesture" and will reject.
|
||||
//
|
||||
// This function sets up some handlers to work around that behavior.
|
||||
private installWebKitWriteTextWorkaround(): void {
|
||||
const handler = () => {
|
||||
const currentWritePromise = new DeferredPromise<string>();
|
||||
|
||||
// Cancel the previous promise since we just created a new one in response to this new event
|
||||
if (this.webKitPendingClipboardWritePromise && !this.webKitPendingClipboardWritePromise.isSettled) {
|
||||
this.webKitPendingClipboardWritePromise.cancel();
|
||||
}
|
||||
this.webKitPendingClipboardWritePromise = currentWritePromise;
|
||||
|
||||
// The ctor of ClipboardItem allows you to pass in a promise that will resolve to a string.
|
||||
// This allows us to pass in a Promise that will either be cancelled by another event or
|
||||
// resolved with the contents of the first call to this.writeText.
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem/ClipboardItem#parameters
|
||||
navigator.clipboard.write([new ClipboardItem({
|
||||
'text/plain': currentWritePromise.p,
|
||||
})]).catch(async err => {
|
||||
if (!(err instanceof Error) || err.name !== 'NotAllowedError' || !currentWritePromise.isRejected) {
|
||||
this.logService.error(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (this.layoutService.hasContainer) {
|
||||
this._register(addDisposableListener(this.layoutService.container, 'click', handler));
|
||||
this._register(addDisposableListener(this.layoutService.container, 'keydown', handler));
|
||||
}
|
||||
}
|
||||
|
||||
async writeText(text: string, type?: string): Promise<void> {
|
||||
|
||||
// With type: only in-memory is supported
|
||||
@@ -27,6 +84,13 @@ export class BrowserClipboardService implements IClipboardService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.webKitPendingClipboardWritePromise) {
|
||||
// For Safari, we complete this Promise which allows the call to `navigator.clipboard.write()`
|
||||
// above to resolve and successfully copy to the clipboard. If we let this continue, Safari
|
||||
// would throw an error because this call stack doesn't appear to originate from a user gesture.
|
||||
return this.webKitPendingClipboardWritePromise.complete(text);
|
||||
}
|
||||
|
||||
// Guard access to navigator.clipboard with try/catch
|
||||
// as we have seen DOMExceptions in certain browsers
|
||||
// due to security policies.
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ClipboardData, IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
|
||||
export class TestClipboardService implements IClipboardService {
|
||||
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private text: string | undefined = undefined;
|
||||
|
||||
async write(data: ClipboardData, type?: string): Promise<void> { // {{SQL CARBON EDIT}} - add method
|
||||
}
|
||||
|
||||
async writeText(text: string, type?: string): Promise<void> {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
async readText(type?: string): Promise<string> {
|
||||
return this.text ?? '';
|
||||
}
|
||||
|
||||
private findText: string | undefined = undefined;
|
||||
|
||||
async readFindText(): Promise<string> {
|
||||
return this.findText ?? '';
|
||||
}
|
||||
|
||||
async writeFindText(text: string): Promise<void> {
|
||||
this.findText = text;
|
||||
}
|
||||
|
||||
private resources: URI[] | undefined = undefined;
|
||||
|
||||
async writeResources(resources: URI[]): Promise<void> {
|
||||
this.resources = resources;
|
||||
}
|
||||
|
||||
async readResources(): Promise<URI[]> {
|
||||
return this.resources ?? [];
|
||||
}
|
||||
|
||||
async hasResources(): Promise<boolean> {
|
||||
return Array.isArray(this.resources) && this.resources.length > 0;
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,10 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
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 { Extensions, IConfigurationRegistry, overrideIdentifierFromKey, OVERRIDE_PROPERTY_PATTERN } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
|
||||
export const IConfigurationService = createDecorator<IConfigurationService>('configurationService');
|
||||
@@ -27,6 +23,16 @@ export interface IConfigurationOverrides {
|
||||
resource?: URI | null;
|
||||
}
|
||||
|
||||
export function isConfigurationUpdateOverrides(thing: any): thing is IConfigurationUpdateOverrides {
|
||||
return thing
|
||||
&& typeof thing === 'object'
|
||||
&& (!thing.overrideIdentifiers || types.isArray(thing.overrideIdentifiers))
|
||||
&& !thing.overrideIdentifier
|
||||
&& (!thing.resource || thing.resource instanceof URI);
|
||||
}
|
||||
|
||||
export type IConfigurationUpdateOverrides = Omit<IConfigurationOverrides, 'overrideIdentifier'> & { overrideIdentifiers?: string[] | null };
|
||||
|
||||
export const enum ConfigurationTarget {
|
||||
USER = 1,
|
||||
USER_LOCAL,
|
||||
@@ -76,13 +82,13 @@ export interface IConfigurationValue<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 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[];
|
||||
}
|
||||
@@ -107,10 +113,28 @@ export interface IConfigurationService {
|
||||
getValue<T>(overrides: IConfigurationOverrides): T;
|
||||
getValue<T>(section: string, overrides: IConfigurationOverrides): T;
|
||||
|
||||
/**
|
||||
* Update a configuration value.
|
||||
*
|
||||
* Use `target` to update the configuration in a specific `ConfigurationTarget`.
|
||||
*
|
||||
* Use `overrides` to update the configuration for a resource or for override identifiers or both.
|
||||
*
|
||||
* Passing a resource through overrides will update the configuration in the workspace folder containing that resource.
|
||||
*
|
||||
* *Note 1:* Updating configuraiton to a default value will remove the configuration from the requested target. If not target is passed, it will be removed from all writeable targets.
|
||||
*
|
||||
* *Note 2:* Use `undefined` value to remove the configuration from the given target. If not target is passed, it will be removed from all writeable targets.
|
||||
*
|
||||
* Use `donotNotifyError` and set it to `true` to surpresss errors.
|
||||
*
|
||||
* @param key setting to be updated
|
||||
* @param value The new value
|
||||
*/
|
||||
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>;
|
||||
updateValue(key: string, value: any, overrides: IConfigurationOverrides | IConfigurationUpdateOverrides): Promise<void>;
|
||||
updateValue(key: string, value: any, overrides: IConfigurationOverrides | IConfigurationUpdateOverrides, target: ConfigurationTarget, donotNotifyError?: boolean): Promise<void>;
|
||||
|
||||
inspect<T>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<Readonly<T>>;
|
||||
|
||||
@@ -151,89 +175,6 @@ export interface IConfigurationCompareResult {
|
||||
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);
|
||||
|
||||
@@ -337,27 +278,6 @@ export function merge(base: any, add: any, overwrite: boolean): void {
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -370,3 +290,7 @@ export function getMigratedSettingValue<T>(configurationService: IConfigurationS
|
||||
return setting.defaultValue!;
|
||||
}
|
||||
}
|
||||
|
||||
export function getLanguageTagSettingPlainKey(settingKey: string) {
|
||||
return settingKey.replace(/[\[\]]/g, '');
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as arrays from 'vs/base/common/arrays';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import * as json from 'vs/base/common/json';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
@@ -12,9 +13,9 @@ import * as objects from 'vs/base/common/objects';
|
||||
import { IExtUri } from 'vs/base/common/resources';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { addToValueTree, compare, ConfigurationTarget, getConfigurationKeys, getConfigurationValue, getDefaultValues, IConfigurationChange, IConfigurationChangeEvent, IConfigurationData, IConfigurationModel, IConfigurationOverrides, IConfigurationValue, IOverrides, removeFromValueTree, toOverrides, toValuesTree } from 'vs/platform/configuration/common/configuration';
|
||||
import { ConfigurationScope, Extensions, IConfigurationPropertySchema, IConfigurationRegistry, overrideIdentifierFromKey, OVERRIDE_PROPERTY_PATTERN } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { addToValueTree, ConfigurationTarget, getConfigurationValue, IConfigurationChange, IConfigurationChangeEvent, IConfigurationCompareResult, IConfigurationData, IConfigurationModel, IConfigurationOverrides, IConfigurationUpdateOverrides, IConfigurationValue, IOverrides, removeFromValueTree, toValuesTree } from 'vs/platform/configuration/common/configuration';
|
||||
import { ConfigurationScope, Extensions, IConfigurationPropertySchema, IConfigurationRegistry, overrideIdentifiersFromKey, OVERRIDE_PROPERTY_REGEX } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { FileOperation, IFileService } from 'vs/platform/files/common/files';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { Workspace } from 'vs/platform/workspace/common/workspace';
|
||||
|
||||
@@ -58,12 +59,21 @@ export class ConfigurationModel implements IConfigurationModel {
|
||||
}
|
||||
|
||||
getKeysForOverrideIdentifier(identifier: string): string[] {
|
||||
const keys: string[] = [];
|
||||
for (const override of this.overrides) {
|
||||
if (override.identifiers.indexOf(identifier) !== -1) {
|
||||
return override.keys;
|
||||
if (override.identifiers.includes(identifier)) {
|
||||
keys.push(...override.keys);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
return arrays.distinct(keys);
|
||||
}
|
||||
|
||||
getAllOverrideIdentifiers(): string[] {
|
||||
const result: string[] = [];
|
||||
for (const override of this.overrides) {
|
||||
result.push(...override.identifiers);
|
||||
}
|
||||
return arrays.distinct(result);
|
||||
}
|
||||
|
||||
override(identifier: string): ConfigurationModel {
|
||||
@@ -87,6 +97,8 @@ export class ConfigurationModel implements IConfigurationModel {
|
||||
const [override] = overrides.filter(o => arrays.equals(o.identifiers, otherOverride.identifiers));
|
||||
if (override) {
|
||||
this.mergeContents(override.contents, otherOverride.contents);
|
||||
override.keys.push(...otherOverride.keys);
|
||||
override.keys = arrays.distinct(override.keys);
|
||||
} else {
|
||||
overrides.push(objects.deepClone(otherOverride));
|
||||
}
|
||||
@@ -156,12 +168,27 @@ export class ConfigurationModel implements IConfigurationModel {
|
||||
}
|
||||
|
||||
private getContentsForOverrideIdentifer(identifier: string): any {
|
||||
let contentsForIdentifierOnly: IStringDictionary<any> | null = null;
|
||||
let contents: IStringDictionary<any> | null = null;
|
||||
const mergeContents = (contentsToMerge: any) => {
|
||||
if (contentsToMerge) {
|
||||
if (contents) {
|
||||
this.mergeContents(contents, contentsToMerge);
|
||||
} else {
|
||||
contents = objects.deepClone(contentsToMerge);
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const override of this.overrides) {
|
||||
if (override.identifiers.indexOf(identifier) !== -1) {
|
||||
return override.contents;
|
||||
if (arrays.equals(override.identifiers, [identifier])) {
|
||||
contentsForIdentifierOnly = override.contents;
|
||||
} else if (override.identifiers.includes(identifier)) {
|
||||
mergeContents(override.contents);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
// Merge contents of the identifier only at the end to take precedence.
|
||||
mergeContents(contentsForIdentifierOnly);
|
||||
return contents;
|
||||
}
|
||||
|
||||
toJSON(): IConfigurationModel {
|
||||
@@ -207,19 +234,27 @@ export class ConfigurationModel implements IConfigurationModel {
|
||||
|
||||
export class DefaultConfigurationModel extends ConfigurationModel {
|
||||
|
||||
constructor() {
|
||||
const contents = getDefaultValues();
|
||||
const keys = getConfigurationKeys();
|
||||
constructor(configurationDefaultsOverrides: IStringDictionary<any> = {}) {
|
||||
const properties = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties();
|
||||
const keys = Object.keys(properties);
|
||||
const contents: any = Object.create(null);
|
||||
const overrides: IOverrides[] = [];
|
||||
|
||||
for (const key in properties) {
|
||||
const defaultOverrideValue = configurationDefaultsOverrides[key];
|
||||
const value = defaultOverrideValue !== undefined ? defaultOverrideValue : properties[key].default;
|
||||
addToValueTree(contents, key, value, message => console.error(`Conflict in default settings: ${message}`));
|
||||
}
|
||||
for (const key of Object.keys(contents)) {
|
||||
if (OVERRIDE_PROPERTY_PATTERN.test(key)) {
|
||||
if (OVERRIDE_PROPERTY_REGEX.test(key)) {
|
||||
overrides.push({
|
||||
identifiers: [overrideIdentifierFromKey(key).trim()],
|
||||
identifiers: overrideIdentifiersFromKey(key),
|
||||
keys: Object.keys(contents[key]),
|
||||
contents: toValuesTree(contents[key], message => console.error(`Conflict in default settings file: ${message}`)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
super(contents, keys, overrides);
|
||||
}
|
||||
}
|
||||
@@ -333,18 +368,18 @@ export class ConfigurationModelParser {
|
||||
raw = filtered.raw;
|
||||
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}`));
|
||||
const overrides = this.toOverrides(raw, message => console.error(`Conflict in settings file ${this._name}: ${message}`));
|
||||
return { contents, keys, overrides, restricted: filtered.restricted };
|
||||
}
|
||||
|
||||
private filter(properties: any, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema | undefined }, filterOverriddenProperties: boolean, options?: ConfigurationParseOptions): { raw: {}, restricted: string[] } {
|
||||
private filter(properties: any, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema | undefined }, filterOverriddenProperties: boolean, options?: ConfigurationParseOptions): { raw: {}; restricted: string[] } {
|
||||
if (!options?.scopes && !options?.skipRestricted) {
|
||||
return { raw: properties, restricted: [] };
|
||||
}
|
||||
const raw: any = {};
|
||||
const restricted: string[] = [];
|
||||
for (let key in properties) {
|
||||
if (OVERRIDE_PROPERTY_PATTERN.test(key) && filterOverriddenProperties) {
|
||||
if (OVERRIDE_PROPERTY_REGEX.test(key) && filterOverriddenProperties) {
|
||||
const result = this.filter(properties[key], configurationProperties, false, options);
|
||||
raw[key] = result.raw;
|
||||
restricted.push(...result.restricted);
|
||||
@@ -365,6 +400,24 @@ export class ConfigurationModelParser {
|
||||
return { raw, restricted };
|
||||
}
|
||||
|
||||
private toOverrides(raw: any, conflictReporter: (message: string) => void): IOverrides[] {
|
||||
const overrides: IOverrides[] = [];
|
||||
for (const key of Object.keys(raw)) {
|
||||
if (OVERRIDE_PROPERTY_REGEX.test(key)) {
|
||||
const overrideRaw: any = {};
|
||||
for (const keyInOverrideRaw in raw[key]) {
|
||||
overrideRaw[keyInOverrideRaw] = raw[key][keyInOverrideRaw];
|
||||
}
|
||||
overrides.push({
|
||||
identifiers: overrideIdentifiersFromKey(key),
|
||||
keys: Object.keys(overrideRaw),
|
||||
contents: toValuesTree(overrideRaw, conflictReporter)
|
||||
});
|
||||
}
|
||||
}
|
||||
return overrides;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class UserSettings extends Disposable {
|
||||
@@ -386,7 +439,10 @@ export class UserSettings extends Disposable {
|
||||
this._register(this.fileService.watch(extUri.dirname(this.userSettingsResource)));
|
||||
// Also listen to the resource incase the resource is a symlink - https://github.com/microsoft/vscode/issues/118134
|
||||
this._register(this.fileService.watch(this.userSettingsResource));
|
||||
this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.userSettingsResource))(() => this._onDidChange.fire()));
|
||||
this._register(Event.any(
|
||||
Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.userSettingsResource)),
|
||||
Event.filter(this.fileService.onDidRunOperation, e => (e.isOperation(FileOperation.CREATE) || e.isOperation(FileOperation.COPY) || e.isOperation(FileOperation.DELETE) || e.isOperation(FileOperation.WRITE)) && extUri.isEqual(e.resource, userSettingsResource))
|
||||
)(() => this._onDidChange.fire()));
|
||||
}
|
||||
|
||||
async loadConfiguration(): Promise<ConfigurationModel> {
|
||||
@@ -431,7 +487,7 @@ export class Configuration {
|
||||
return consolidateConfigurationModel.getValue(section);
|
||||
}
|
||||
|
||||
updateValue(key: string, value: any, overrides: IConfigurationOverrides = {}): void {
|
||||
updateValue(key: string, value: any, overrides: IConfigurationUpdateOverrides = {}): void {
|
||||
let memoryConfiguration: ConfigurationModel | undefined;
|
||||
if (overrides.resource) {
|
||||
memoryConfiguration = this._memoryConfigurationByResource.get(overrides.resource);
|
||||
@@ -542,11 +598,14 @@ export class Configuration {
|
||||
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);
|
||||
compareAndUpdateDefaultConfiguration(defaults: ConfigurationModel, keys?: string[]): IConfigurationChange {
|
||||
const overrides: [string, string[]][] = [];
|
||||
if (!keys) {
|
||||
const { added, updated, removed } = compare(this._defaultConfiguration, defaults);
|
||||
keys = [...added, ...updated, ...removed];
|
||||
}
|
||||
for (const key of keys) {
|
||||
for (const overrideIdentifier of overrideIdentifiersFromKey(key)) {
|
||||
const fromKeys = this._defaultConfiguration.getKeysForOverrideIdentifier(overrideIdentifier);
|
||||
const toKeys = defaults.getKeysForOverrideIdentifier(overrideIdentifier);
|
||||
const keys = [
|
||||
@@ -554,8 +613,9 @@ export class Configuration {
|
||||
...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];
|
||||
});
|
||||
overrides.push([overrideIdentifier, keys]);
|
||||
}
|
||||
}
|
||||
this.updateDefaultConfiguration(defaults);
|
||||
return { keys, overrides };
|
||||
}
|
||||
@@ -732,6 +792,15 @@ export class Configuration {
|
||||
return [...keys.values()];
|
||||
}
|
||||
|
||||
protected allOverrideIdentifiers(): string[] {
|
||||
const keys: Set<string> = new Set<string>();
|
||||
this._defaultConfiguration.freeze().getAllOverrideIdentifiers().forEach(key => keys.add(key));
|
||||
this.userConfiguration.freeze().getAllOverrideIdentifiers().forEach(key => keys.add(key));
|
||||
this._workspaceConfiguration.freeze().getAllOverrideIdentifiers().forEach(key => keys.add(key));
|
||||
this._folderConfigurations.forEach(folderConfiguraiton => folderConfiguraiton.freeze().getAllOverrideIdentifiers().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));
|
||||
@@ -786,7 +855,7 @@ export class ConfigurationChangeEvent implements IConfigurationChangeEvent {
|
||||
source!: ConfigurationTarget;
|
||||
sourceConfig: any;
|
||||
|
||||
constructor(readonly change: IConfigurationChange, private readonly previous: { workspace?: Workspace, data: IConfigurationData } | undefined, private readonly currentConfiguraiton: Configuration, private readonly currentWorkspace?: Workspace) {
|
||||
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)));
|
||||
@@ -839,3 +908,59 @@ export class AllKeysConfigurationChangeEvent extends ConfigurationChangeEvent {
|
||||
this.sourceConfig = sourceConfig;
|
||||
}
|
||||
}
|
||||
|
||||
function compare(from: ConfigurationModel | undefined, to: ConfigurationModel | undefined): IConfigurationCompareResult {
|
||||
const { added, removed, updated } = compareConfigurationContents(to, from);
|
||||
const overrides: [string, string[]][] = [];
|
||||
|
||||
const fromOverrideIdentifiers = from?.getAllOverrideIdentifiers() || [];
|
||||
const toOverrideIdentifiers = to?.getAllOverrideIdentifiers() || [];
|
||||
|
||||
if (to) {
|
||||
const addedOverrideIdentifiers = toOverrideIdentifiers.filter(key => !fromOverrideIdentifiers.includes(key));
|
||||
for (const identifier of addedOverrideIdentifiers) {
|
||||
overrides.push([identifier, to.getKeysForOverrideIdentifier(identifier)]);
|
||||
}
|
||||
}
|
||||
|
||||
if (from) {
|
||||
const removedOverrideIdentifiers = fromOverrideIdentifiers.filter(key => !toOverrideIdentifiers.includes(key));
|
||||
for (const identifier of removedOverrideIdentifiers) {
|
||||
overrides.push([identifier, from.getKeysForOverrideIdentifier(identifier)]);
|
||||
}
|
||||
}
|
||||
|
||||
if (to && from) {
|
||||
for (const identifier of fromOverrideIdentifiers) {
|
||||
if (toOverrideIdentifiers.includes(identifier)) {
|
||||
const result = compareConfigurationContents({ contents: from.getOverrideValue(undefined, identifier) || {}, keys: from.getKeysForOverrideIdentifier(identifier) }, { contents: to.getOverrideValue(undefined, identifier) || {}, keys: to.getKeysForOverrideIdentifier(identifier) });
|
||||
overrides.push([identifier, [...result.added, ...result.removed, ...result.updated]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { added, removed, updated, overrides };
|
||||
}
|
||||
|
||||
function compareConfigurationContents(to: { keys: string[]; contents: any } | undefined, from: { keys: string[]; contents: any } | undefined) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { added, removed, updated };
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import * as nls from 'vs/nls';
|
||||
import { getLanguageTagSettingPlainKey } from 'vs/platform/configuration/common/configuration';
|
||||
import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
|
||||
@@ -43,17 +44,22 @@ export interface IConfigurationRegistry {
|
||||
* - registering the configurations to add
|
||||
* - dereigstering the configurations to remove
|
||||
*/
|
||||
updateConfigurations(configurations: { add: IConfigurationNode[], remove: IConfigurationNode[] }): void;
|
||||
updateConfigurations(configurations: { add: IConfigurationNode[]; remove: IConfigurationNode[] }): void;
|
||||
|
||||
/**
|
||||
* Register multiple default configurations to the registry.
|
||||
*/
|
||||
registerDefaultConfigurations(defaultConfigurations: IStringDictionary<any>[]): void;
|
||||
registerDefaultConfigurations(defaultConfigurations: IConfigurationDefaults[]): void;
|
||||
|
||||
/**
|
||||
* Deregister multiple default configurations from the registry.
|
||||
*/
|
||||
deregisterDefaultConfigurations(defaultConfigurations: IStringDictionary<any>[]): void;
|
||||
deregisterDefaultConfigurations(defaultConfigurations: IConfigurationDefaults[]): void;
|
||||
|
||||
/**
|
||||
* Return the registered configuration defaults overrides
|
||||
*/
|
||||
getConfigurationDefaultsOverrides(): Map<string, IConfigurationDefaultOverride>;
|
||||
|
||||
/**
|
||||
* Signal that the schema of a configuration setting has changes. It is currently only supported to change enumeration values.
|
||||
@@ -62,16 +68,16 @@ export interface IConfigurationRegistry {
|
||||
notifyConfigurationSchemaUpdated(...configurations: IConfigurationNode[]): void;
|
||||
|
||||
/**
|
||||
* Event that fires whenver a configuration has been
|
||||
* Event that fires whenever a configuration has been
|
||||
* registered.
|
||||
*/
|
||||
onDidSchemaChange: Event<void>;
|
||||
readonly onDidSchemaChange: Event<void>;
|
||||
|
||||
/**
|
||||
* Event that fires whenver a configuration has been
|
||||
* Event that fires whenever a configuration has been
|
||||
* registered.
|
||||
*/
|
||||
onDidUpdateConfiguration: Event<string[]>;
|
||||
readonly onDidUpdateConfiguration: Event<{ properties: string[]; defaultsOverrides?: boolean }>;
|
||||
|
||||
/**
|
||||
* Returns all configuration nodes contributed to this registry.
|
||||
@@ -81,12 +87,12 @@ export interface IConfigurationRegistry {
|
||||
/**
|
||||
* Returns all configurations settings of all configuration nodes contributed to this registry.
|
||||
*/
|
||||
getConfigurationProperties(): { [qualifiedKey: string]: IConfigurationPropertySchema };
|
||||
getConfigurationProperties(): IStringDictionary<IRegisteredConfigurationPropertySchema>;
|
||||
|
||||
/**
|
||||
* Returns all excluded configurations settings of all configuration nodes contributed to this registry.
|
||||
*/
|
||||
getExcludedConfigurationProperties(): { [qualifiedKey: string]: IConfigurationPropertySchema };
|
||||
getExcludedConfigurationProperties(): IStringDictionary<IRegisteredConfigurationPropertySchema>;
|
||||
|
||||
/**
|
||||
* Register the identifiers for editor configurations
|
||||
@@ -131,8 +137,16 @@ export interface IConfigurationPropertySchema extends IJSONSchema {
|
||||
*/
|
||||
restricted?: boolean;
|
||||
|
||||
/**
|
||||
* When `false` this property is excluded from the registry. Default is to include.
|
||||
*/
|
||||
included?: boolean;
|
||||
|
||||
/**
|
||||
* List of tags associated to the property.
|
||||
* - A tag can be used for filtering
|
||||
* - Use `experimental` tag for marking the setting as experimental. **Note:** Defaults of experimental settings can be changed by the running experiments.
|
||||
*/
|
||||
tags?: string[];
|
||||
|
||||
/**
|
||||
@@ -145,6 +159,9 @@ export interface IConfigurationPropertySchema extends IJSONSchema {
|
||||
*/
|
||||
disallowSyncIgnore?: boolean;
|
||||
|
||||
/**
|
||||
* Labels for enumeration items
|
||||
*/
|
||||
enumItemLabels?: string[];
|
||||
|
||||
/**
|
||||
@@ -152,11 +169,17 @@ export interface IConfigurationPropertySchema extends IJSONSchema {
|
||||
* Otherwise, the presentation format defaults to `singleline`.
|
||||
*/
|
||||
editPresentation?: EditPresentationTypes;
|
||||
|
||||
/**
|
||||
* When specified, gives an order number for the setting
|
||||
* within the settings editor. Otherwise, the setting is placed at the end.
|
||||
*/
|
||||
order?: number;
|
||||
}
|
||||
|
||||
export interface IConfigurationExtensionInfo {
|
||||
export interface IExtensionInfo {
|
||||
id: string;
|
||||
restrictedConfigurations?: string[];
|
||||
displayName?: string;
|
||||
}
|
||||
|
||||
export interface IConfigurationNode {
|
||||
@@ -165,41 +188,56 @@ export interface IConfigurationNode {
|
||||
type?: string | string[];
|
||||
title?: string;
|
||||
description?: string;
|
||||
properties?: { [path: string]: IConfigurationPropertySchema; };
|
||||
properties?: IStringDictionary<IConfigurationPropertySchema>;
|
||||
allOf?: IConfigurationNode[];
|
||||
scope?: ConfigurationScope;
|
||||
extensionInfo?: IConfigurationExtensionInfo;
|
||||
extensionInfo?: IExtensionInfo;
|
||||
restrictedProperties?: string[];
|
||||
}
|
||||
|
||||
export const allSettings: { properties: IStringDictionary<IConfigurationPropertySchema>, patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
|
||||
export const applicationSettings: { properties: IStringDictionary<IConfigurationPropertySchema>, patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
|
||||
export const machineSettings: { properties: IStringDictionary<IConfigurationPropertySchema>, patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
|
||||
export const machineOverridableSettings: { properties: IStringDictionary<IConfigurationPropertySchema>, patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
|
||||
export const windowSettings: { properties: IStringDictionary<IConfigurationPropertySchema>, patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
|
||||
export const resourceSettings: { properties: IStringDictionary<IConfigurationPropertySchema>, patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
|
||||
export interface IConfigurationDefaults {
|
||||
overrides: IStringDictionary<any>;
|
||||
source?: IExtensionInfo | string;
|
||||
}
|
||||
|
||||
export type IRegisteredConfigurationPropertySchema = IConfigurationPropertySchema & {
|
||||
defaultDefaultValue?: any;
|
||||
source?: IExtensionInfo;
|
||||
defaultValueSource?: IExtensionInfo | string;
|
||||
};
|
||||
|
||||
export type IConfigurationDefaultOverride = { value: any; source?: IExtensionInfo | string };
|
||||
|
||||
export const allSettings: { properties: IStringDictionary<IConfigurationPropertySchema>; patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
|
||||
export const applicationSettings: { properties: IStringDictionary<IConfigurationPropertySchema>; patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
|
||||
export const machineSettings: { properties: IStringDictionary<IConfigurationPropertySchema>; patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
|
||||
export const machineOverridableSettings: { properties: IStringDictionary<IConfigurationPropertySchema>; patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
|
||||
export const windowSettings: { properties: IStringDictionary<IConfigurationPropertySchema>; patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
|
||||
export const resourceSettings: { properties: IStringDictionary<IConfigurationPropertySchema>; patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
|
||||
|
||||
export const resourceLanguageSettingsSchemaId = 'vscode://schemas/settings/resourceLanguage';
|
||||
export const configurationDefaultsSchemaId = 'vscode://schemas/settings/configurationDefaults';
|
||||
|
||||
const contributionRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
|
||||
|
||||
class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
|
||||
private readonly defaultValues: IStringDictionary<any>;
|
||||
private readonly configurationDefaultsOverrides: Map<string, IConfigurationDefaultOverride>;
|
||||
private readonly defaultLanguageConfigurationOverridesNode: IConfigurationNode;
|
||||
private readonly configurationContributors: IConfigurationNode[];
|
||||
private readonly configurationProperties: { [qualifiedKey: string]: IJSONSchema };
|
||||
private readonly excludedConfigurationProperties: { [qualifiedKey: string]: IJSONSchema };
|
||||
private readonly configurationProperties: IStringDictionary<IRegisteredConfigurationPropertySchema>;
|
||||
private readonly excludedConfigurationProperties: IStringDictionary<IRegisteredConfigurationPropertySchema>;
|
||||
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;
|
||||
private readonly _onDidUpdateConfiguration = new Emitter<{ properties: string[]; defaultsOverrides?: boolean }>();
|
||||
readonly onDidUpdateConfiguration = this._onDidUpdateConfiguration.event;
|
||||
|
||||
constructor() {
|
||||
this.defaultValues = {};
|
||||
this.configurationDefaultsOverrides = new Map<string, IConfigurationDefaultOverride>();
|
||||
this.defaultLanguageConfigurationOverridesNode = {
|
||||
id: 'defaultOverrides',
|
||||
title: nls.localize('defaultLanguageConfigurationOverrides.title', "Default Language Configuration Overrides"),
|
||||
@@ -211,6 +249,7 @@ class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
this.excludedConfigurationProperties = {};
|
||||
|
||||
contributionRegistry.registerSchema(resourceLanguageSettingsSchemaId, this.resourceLanguageSettingsSchema);
|
||||
this.registerOverridePropertyPatternKey();
|
||||
}
|
||||
|
||||
public registerConfiguration(configuration: IConfigurationNode, validate: boolean = true): void {
|
||||
@@ -222,7 +261,7 @@ class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
|
||||
contributionRegistry.registerSchema(resourceLanguageSettingsSchemaId, this.resourceLanguageSettingsSchema);
|
||||
this._onDidSchemaChange.fire();
|
||||
this._onDidUpdateConfiguration.fire(properties);
|
||||
this._onDidUpdateConfiguration.fire({ properties });
|
||||
}
|
||||
|
||||
public deregisterConfigurations(configurations: IConfigurationNode[]): void {
|
||||
@@ -230,40 +269,44 @@ class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
|
||||
contributionRegistry.registerSchema(resourceLanguageSettingsSchemaId, this.resourceLanguageSettingsSchema);
|
||||
this._onDidSchemaChange.fire();
|
||||
this._onDidUpdateConfiguration.fire(properties);
|
||||
this._onDidUpdateConfiguration.fire({ properties });
|
||||
}
|
||||
|
||||
public updateConfigurations({ add, remove }: { add: IConfigurationNode[], remove: IConfigurationNode[] }): void {
|
||||
public updateConfigurations({ add, remove }: { add: IConfigurationNode[]; remove: IConfigurationNode[] }): void {
|
||||
const properties = [];
|
||||
properties.push(...this.doDeregisterConfigurations(remove));
|
||||
properties.push(...this.doRegisterConfigurations(add, false));
|
||||
|
||||
contributionRegistry.registerSchema(resourceLanguageSettingsSchemaId, this.resourceLanguageSettingsSchema);
|
||||
this._onDidSchemaChange.fire();
|
||||
this._onDidUpdateConfiguration.fire(distinct(properties));
|
||||
this._onDidUpdateConfiguration.fire({ properties: distinct(properties) });
|
||||
}
|
||||
|
||||
public registerDefaultConfigurations(defaultConfigurations: IStringDictionary<any>[]): void {
|
||||
public registerDefaultConfigurations(configurationDefaults: IConfigurationDefaults[]): void {
|
||||
const properties: string[] = [];
|
||||
const overrideIdentifiers: string[] = [];
|
||||
|
||||
for (const defaultConfiguration of defaultConfigurations) {
|
||||
for (const key in defaultConfiguration) {
|
||||
for (const { overrides, source } of configurationDefaults) {
|
||||
for (const key in overrides) {
|
||||
properties.push(key);
|
||||
|
||||
if (OVERRIDE_PROPERTY_PATTERN.test(key)) {
|
||||
this.defaultValues[key] = { ...(this.defaultValues[key] || {}), ...defaultConfiguration[key] };
|
||||
const property: IConfigurationPropertySchema = {
|
||||
if (OVERRIDE_PROPERTY_REGEX.test(key)) {
|
||||
const defaultValue = { ...(this.configurationDefaultsOverrides.get(key)?.value || {}), ...overrides[key] };
|
||||
this.configurationDefaultsOverrides.set(key, { source, value: defaultValue });
|
||||
const plainKey = getLanguageTagSettingPlainKey(key);
|
||||
const property: IRegisteredConfigurationPropertySchema = {
|
||||
type: 'object',
|
||||
default: this.defaultValues[key],
|
||||
description: nls.localize('defaultLanguageConfiguration.description', "Configure settings to be overridden for {0} language.", key),
|
||||
$ref: resourceLanguageSettingsSchemaId
|
||||
default: defaultValue,
|
||||
description: nls.localize('defaultLanguageConfiguration.description', "Configure settings to be overridden for the {0} language.", plainKey),
|
||||
$ref: resourceLanguageSettingsSchemaId,
|
||||
defaultDefaultValue: defaultValue,
|
||||
source: types.isString(source) ? undefined : source,
|
||||
};
|
||||
overrideIdentifiers.push(overrideIdentifierFromKey(key));
|
||||
overrideIdentifiers.push(...overrideIdentifiersFromKey(key));
|
||||
this.configurationProperties[key] = property;
|
||||
this.defaultLanguageConfigurationOverridesNode.properties![key] = property;
|
||||
} else {
|
||||
this.defaultValues[key] = defaultConfiguration[key];
|
||||
this.configurationDefaultsOverrides.set(key, { value: overrides[key], source });
|
||||
const property = this.configurationProperties[key];
|
||||
if (property) {
|
||||
this.updatePropertyDefaultValue(key, property);
|
||||
@@ -275,16 +318,22 @@ class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
|
||||
this.registerOverrideIdentifiers(overrideIdentifiers);
|
||||
this._onDidSchemaChange.fire();
|
||||
this._onDidUpdateConfiguration.fire(properties);
|
||||
this._onDidUpdateConfiguration.fire({ properties, defaultsOverrides: true });
|
||||
}
|
||||
|
||||
public deregisterDefaultConfigurations(defaultConfigurations: IStringDictionary<any>[]): void {
|
||||
public deregisterDefaultConfigurations(defaultConfigurations: IConfigurationDefaults[]): void {
|
||||
const properties: string[] = [];
|
||||
for (const defaultConfiguration of defaultConfigurations) {
|
||||
for (const key in defaultConfiguration) {
|
||||
for (const { overrides, source } of defaultConfigurations) {
|
||||
for (const key in overrides) {
|
||||
const configurationDefaultsOverride = this.configurationDefaultsOverrides.get(key);
|
||||
const id = types.isString(source) ? source : source?.id;
|
||||
const configurationDefaultsOverrideSourceId = types.isString(configurationDefaultsOverride?.source) ? configurationDefaultsOverride?.source : configurationDefaultsOverride?.source?.id;
|
||||
if (id !== configurationDefaultsOverrideSourceId) {
|
||||
continue;
|
||||
}
|
||||
properties.push(key);
|
||||
delete this.defaultValues[key];
|
||||
if (OVERRIDE_PROPERTY_PATTERN.test(key)) {
|
||||
this.configurationDefaultsOverrides.delete(key);
|
||||
if (OVERRIDE_PROPERTY_REGEX.test(key)) {
|
||||
delete this.configurationProperties[key];
|
||||
delete this.defaultLanguageConfigurationOverridesNode.properties![key];
|
||||
} else {
|
||||
@@ -299,7 +348,7 @@ class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
|
||||
this.updateOverridePropertyPatternKey();
|
||||
this._onDidSchemaChange.fire();
|
||||
this._onDidUpdateConfiguration.fire(properties);
|
||||
this._onDidUpdateConfiguration.fire({ properties, defaultsOverrides: true });
|
||||
}
|
||||
|
||||
public notifyConfigurationSchemaUpdated(...configurations: IConfigurationNode[]) {
|
||||
@@ -316,7 +365,7 @@ class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
private doRegisterConfigurations(configurations: IConfigurationNode[], validate: boolean): string[] {
|
||||
const properties: string[] = [];
|
||||
configurations.forEach(configuration => {
|
||||
properties.push(...this.validateAndRegisterProperties(configuration, validate, configuration.extensionInfo)); // fills in defaults
|
||||
properties.push(...this.validateAndRegisterProperties(configuration, validate, configuration.extensionInfo, configuration.restrictedProperties)); // fills in defaults
|
||||
this.configurationContributors.push(configuration);
|
||||
this.registerJSONConfiguration(configuration);
|
||||
});
|
||||
@@ -347,7 +396,7 @@ class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
return properties;
|
||||
}
|
||||
|
||||
private validateAndRegisterProperties(configuration: IConfigurationNode, validate: boolean = true, extensionInfo?: IConfigurationExtensionInfo, scope: ConfigurationScope = ConfigurationScope.WINDOW): string[] {
|
||||
private validateAndRegisterProperties(configuration: IConfigurationNode, validate: boolean = true, extensionInfo: IExtensionInfo | undefined, restrictedProperties: string[] | undefined, scope: ConfigurationScope = ConfigurationScope.WINDOW): string[] {
|
||||
scope = types.isUndefinedOrNull(configuration.scope) ? scope : configuration.scope;
|
||||
let propertyKeys: string[] = [];
|
||||
let properties = configuration.properties;
|
||||
@@ -358,17 +407,19 @@ class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
continue;
|
||||
}
|
||||
|
||||
const property = properties[key];
|
||||
const property: IRegisteredConfigurationPropertySchema = properties[key];
|
||||
property.source = extensionInfo;
|
||||
|
||||
// update default value
|
||||
property.defaultDefaultValue = properties[key].default;
|
||||
this.updatePropertyDefaultValue(key, property);
|
||||
|
||||
// update scope
|
||||
if (OVERRIDE_PROPERTY_PATTERN.test(key)) {
|
||||
if (OVERRIDE_PROPERTY_REGEX.test(key)) {
|
||||
property.scope = undefined; // No scope for overridable properties `[${identifier}]`
|
||||
} else {
|
||||
property.scope = types.isUndefinedOrNull(property.scope) ? scope : property.scope;
|
||||
property.restricted = types.isUndefinedOrNull(property.restricted) ? !!extensionInfo?.restrictedConfigurations?.includes(key) : property.restricted;
|
||||
property.restricted = types.isUndefinedOrNull(property.restricted) ? !!restrictedProperties?.includes(key) : property.restricted;
|
||||
}
|
||||
|
||||
// Add to properties maps
|
||||
@@ -392,24 +443,29 @@ class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
let subNodes = configuration.allOf;
|
||||
if (subNodes) {
|
||||
for (let node of subNodes) {
|
||||
propertyKeys.push(...this.validateAndRegisterProperties(node, validate, extensionInfo, scope));
|
||||
propertyKeys.push(...this.validateAndRegisterProperties(node, validate, extensionInfo, restrictedProperties, scope));
|
||||
}
|
||||
}
|
||||
return propertyKeys;
|
||||
}
|
||||
|
||||
// TODO: @sandy081 - Remove this method and include required info in getConfigurationProperties
|
||||
getConfigurations(): IConfigurationNode[] {
|
||||
return this.configurationContributors;
|
||||
}
|
||||
|
||||
getConfigurationProperties(): { [qualifiedKey: string]: IConfigurationPropertySchema } {
|
||||
getConfigurationProperties(): IStringDictionary<IRegisteredConfigurationPropertySchema> {
|
||||
return this.configurationProperties;
|
||||
}
|
||||
|
||||
getExcludedConfigurationProperties(): { [qualifiedKey: string]: IConfigurationPropertySchema } {
|
||||
getExcludedConfigurationProperties(): IStringDictionary<IRegisteredConfigurationPropertySchema> {
|
||||
return this.excludedConfigurationProperties;
|
||||
}
|
||||
|
||||
getConfigurationDefaultsOverrides(): Map<string, IConfigurationDefaultOverride> {
|
||||
return this.configurationDefaultsOverrides;
|
||||
}
|
||||
|
||||
private registerJSONConfiguration(configuration: IConfigurationNode) {
|
||||
const register = (configuration: IConfigurationNode) => {
|
||||
let properties = configuration.properties;
|
||||
@@ -469,6 +525,7 @@ class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
case ConfigurationScope.RESOURCE:
|
||||
case ConfigurationScope.LANGUAGE_OVERRIDABLE:
|
||||
delete resourceSettings.properties[key];
|
||||
delete this.resourceLanguageSettingsSchema.properties![key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -493,23 +550,60 @@ class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
this._onDidSchemaChange.fire();
|
||||
}
|
||||
|
||||
private updatePropertyDefaultValue(key: string, property: IConfigurationPropertySchema): void {
|
||||
let defaultValue = this.defaultValues[key];
|
||||
private registerOverridePropertyPatternKey(): void {
|
||||
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,
|
||||
};
|
||||
allSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema;
|
||||
applicationSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema;
|
||||
machineSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema;
|
||||
machineOverridableSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema;
|
||||
windowSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema;
|
||||
resourceSettings.patternProperties[OVERRIDE_PROPERTY_PATTERN] = resourceLanguagePropertiesSchema;
|
||||
this._onDidSchemaChange.fire();
|
||||
}
|
||||
|
||||
private updatePropertyDefaultValue(key: string, property: IRegisteredConfigurationPropertySchema): void {
|
||||
const configurationdefaultOverride = this.configurationDefaultsOverrides.get(key);
|
||||
let defaultValue = configurationdefaultOverride?.value;
|
||||
let defaultSource = configurationdefaultOverride?.source;
|
||||
if (types.isUndefined(defaultValue)) {
|
||||
defaultValue = property.default;
|
||||
defaultValue = property.defaultDefaultValue;
|
||||
defaultSource = undefined;
|
||||
}
|
||||
if (types.isUndefined(defaultValue)) {
|
||||
defaultValue = getDefaultValue(property.type);
|
||||
}
|
||||
property.default = defaultValue;
|
||||
property.defaultValueSource = defaultSource;
|
||||
}
|
||||
}
|
||||
|
||||
const OVERRIDE_PROPERTY = '\\[.*\\]$';
|
||||
export const OVERRIDE_PROPERTY_PATTERN = new RegExp(OVERRIDE_PROPERTY);
|
||||
const OVERRIDE_IDENTIFIER_PATTERN = `\\[([^\\]]+)\\]`;
|
||||
const OVERRIDE_IDENTIFIER_REGEX = new RegExp(OVERRIDE_IDENTIFIER_PATTERN, 'g');
|
||||
export const OVERRIDE_PROPERTY_PATTERN = `^(${OVERRIDE_IDENTIFIER_PATTERN})+$`;
|
||||
export const OVERRIDE_PROPERTY_REGEX = new RegExp(OVERRIDE_PROPERTY_PATTERN);
|
||||
|
||||
export function overrideIdentifierFromKey(key: string): string {
|
||||
return key.substring(1, key.length - 1);
|
||||
export function overrideIdentifiersFromKey(key: string): string[] {
|
||||
const identifiers: string[] = [];
|
||||
if (OVERRIDE_PROPERTY_REGEX.test(key)) {
|
||||
let matches = OVERRIDE_IDENTIFIER_REGEX.exec(key);
|
||||
while (matches?.length) {
|
||||
const identifier = matches[1].trim();
|
||||
if (identifier) {
|
||||
identifiers.push(identifier);
|
||||
}
|
||||
matches = OVERRIDE_IDENTIFIER_REGEX.exec(key);
|
||||
}
|
||||
}
|
||||
return distinct(identifiers);
|
||||
}
|
||||
|
||||
export function keyFromOverrideIdentifiers(overrideIdentifiers: string[]): string {
|
||||
return overrideIdentifiers.reduce((result, overrideIdentifier) => `${result}[${overrideIdentifier}]`, '');
|
||||
}
|
||||
|
||||
export function getDefaultValue(type: string | string[] | undefined): any {
|
||||
@@ -531,7 +625,6 @@ export function getDefaultValue(type: string | string[] | undefined): any {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const configurationRegistry = new ConfigurationRegistry();
|
||||
Registry.add(Extensions.Configuration, configurationRegistry);
|
||||
|
||||
@@ -539,7 +632,7 @@ export function validateProperty(property: string): string | null {
|
||||
if (!property.trim()) {
|
||||
return nls.localize('config.property.empty', "Cannot register an empty property");
|
||||
}
|
||||
if (OVERRIDE_PROPERTY_PATTERN.test(property)) {
|
||||
if (OVERRIDE_PROPERTY_REGEX.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) {
|
||||
|
||||
@@ -34,7 +34,7 @@ export class ConfigurationService extends Disposable implements IConfigurationSe
|
||||
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(Registry.as<IConfigurationRegistry>(Extensions.Configuration).onDidUpdateConfiguration(({ properties }) => this.onDidDefaultConfigurationChange(properties)));
|
||||
this._register(this.userConfiguration.onDidChange(() => this.reloadConfigurationScheduler.schedule()));
|
||||
}
|
||||
|
||||
@@ -89,9 +89,9 @@ export class ConfigurationService extends Disposable implements IConfigurationSe
|
||||
this.trigger(change, previous, ConfigurationTarget.USER);
|
||||
}
|
||||
|
||||
private onDidDefaultConfigurationChange(keys: string[]): void {
|
||||
private onDidDefaultConfigurationChange(properties: string[]): void {
|
||||
const previous = this.configuration.toData();
|
||||
const change = this.configuration.compareAndUpdateDefaultConfiguration(new DefaultConfigurationModel(), keys);
|
||||
const change = this.configuration.compareAndUpdateDefaultConfiguration(new DefaultConfigurationModel(), properties);
|
||||
this.trigger(change, previous, ConfigurationTarget.DEFAULT);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Queue } from 'vs/base/common/async';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { JSONPath, parse, ParseError } from 'vs/base/common/json';
|
||||
import { setProperty } from 'vs/base/common/jsonEdit';
|
||||
import { Edit, FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { FileOperationError, FileOperationResult, IFileService, IWriteFileOptions } from 'vs/platform/files/common/files';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
export const enum UserConfigurationErrorCode {
|
||||
ERROR_INVALID_FILE = 'ERROR_INVALID_FILE',
|
||||
ERROR_FILE_MODIFIED_SINCE = 'ERROR_FILE_MODIFIED_SINCE'
|
||||
}
|
||||
|
||||
export interface IJSONValue {
|
||||
path: JSONPath;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export const UserConfigurationFileServiceId = 'IUserConfigurationFileService';
|
||||
export const IUserConfigurationFileService = createDecorator<IUserConfigurationFileService>(UserConfigurationFileServiceId);
|
||||
|
||||
export interface IUserConfigurationFileService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
updateSettings(value: IJSONValue, formattingOptions: FormattingOptions): Promise<void>;
|
||||
write(value: VSBuffer, options?: IWriteFileOptions): Promise<void>;
|
||||
}
|
||||
|
||||
export class UserConfigurationFileService implements IUserConfigurationFileService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly queue: Queue<void>;
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
) {
|
||||
this.queue = new Queue<void>();
|
||||
}
|
||||
|
||||
async updateSettings(value: IJSONValue, formattingOptions: FormattingOptions): Promise<void> {
|
||||
return this.queue.queue(() => this.doWrite(value, formattingOptions)); // queue up writes to prevent race conditions
|
||||
}
|
||||
|
||||
private async doWrite(jsonValue: IJSONValue, formattingOptions: FormattingOptions): Promise<void> {
|
||||
this.logService.trace(`${UserConfigurationFileServiceId}#write`, this.environmentService.settingsResource.toString(), jsonValue);
|
||||
const { value, mtime, etag } = await this.fileService.readFile(this.environmentService.settingsResource, { atomic: true });
|
||||
let content = value.toString();
|
||||
|
||||
const parseErrors: ParseError[] = [];
|
||||
parse(content, parseErrors, { allowTrailingComma: true, allowEmptyContent: true });
|
||||
if (parseErrors.length) {
|
||||
throw new Error(UserConfigurationErrorCode.ERROR_INVALID_FILE);
|
||||
}
|
||||
|
||||
const edit = this.getEdits(jsonValue, content, formattingOptions)[0];
|
||||
if (edit) {
|
||||
content = content.substring(0, edit.offset) + edit.content + content.substring(edit.offset + edit.length);
|
||||
try {
|
||||
await this.fileService.writeFile(this.environmentService.settingsResource, VSBuffer.fromString(content), { etag, mtime });
|
||||
} catch (error) {
|
||||
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) {
|
||||
throw new Error(UserConfigurationErrorCode.ERROR_FILE_MODIFIED_SINCE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async write(content: VSBuffer, options?: IWriteFileOptions): Promise<void> {
|
||||
// queue up writes to prevent race conditions
|
||||
return this.queue.queue(async () => {
|
||||
await this.fileService.writeFile(this.environmentService.settingsResource, content, options);
|
||||
});
|
||||
}
|
||||
|
||||
private getEdits({ value, path }: IJSONValue, modelContent: string, formattingOptions: FormattingOptions): Edit[] {
|
||||
if (path.length) {
|
||||
return setProperty(modelContent, path, value, formattingOptions);
|
||||
}
|
||||
|
||||
// Without jsonPath, the entire configuration file is being replaced, so we just use JSON.stringify
|
||||
const content = JSON.stringify(value, null, formattingOptions.insertSpaces && formattingOptions.tabSize ? ' '.repeat(formattingOptions.tabSize) : '\t');
|
||||
return [{
|
||||
content,
|
||||
length: modelContent.length,
|
||||
offset: 0
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,18 @@ suite('Configuration', () => {
|
||||
assert.deepStrictEqual(base, { 'a': 1, 'b': 2, 'c': 4 });
|
||||
});
|
||||
|
||||
test('object merge', () => {
|
||||
let base = { 'a': { 'b': 1, 'c': true, 'd': 2 } };
|
||||
merge(base, { 'a': { 'b': undefined, 'c': false, 'e': 'a' } }, true);
|
||||
assert.deepStrictEqual(base, { 'a': { 'b': undefined, 'c': false, 'd': 2, 'e': 'a' } });
|
||||
});
|
||||
|
||||
test('array merge', () => {
|
||||
let base = { 'a': ['b', 'c'] };
|
||||
merge(base, { 'a': ['b', 'd'] }, true);
|
||||
assert.deepStrictEqual(base, { 'a': ['b', 'd'] });
|
||||
});
|
||||
|
||||
test('removeFromValueTree: remove a non existing key', () => {
|
||||
let target = { 'a': { 'b': 2 } };
|
||||
|
||||
|
||||
@@ -12,6 +12,35 @@ import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace';
|
||||
|
||||
suite('ConfigurationModelParser', () => {
|
||||
|
||||
test('parse configuration model with single override identifier', () => {
|
||||
const testObject = new ConfigurationModelParser('');
|
||||
|
||||
testObject.parse(JSON.stringify({ '[x]': { 'a': 1 } }));
|
||||
|
||||
assert.deepStrictEqual(JSON.stringify(testObject.configurationModel.overrides), JSON.stringify([{ identifiers: ['x'], keys: ['a'], contents: { 'a': 1 } }]));
|
||||
});
|
||||
|
||||
test('parse configuration model with multiple override identifiers', () => {
|
||||
const testObject = new ConfigurationModelParser('');
|
||||
|
||||
testObject.parse(JSON.stringify({ '[x][y]': { 'a': 1 } }));
|
||||
|
||||
assert.deepStrictEqual(JSON.stringify(testObject.configurationModel.overrides), JSON.stringify([{ identifiers: ['x', 'y'], keys: ['a'], contents: { 'a': 1 } }]));
|
||||
});
|
||||
|
||||
test('parse configuration model with multiple duplicate override identifiers', () => {
|
||||
const testObject = new ConfigurationModelParser('');
|
||||
|
||||
testObject.parse(JSON.stringify({ '[x][y][x][z]': { 'a': 1 } }));
|
||||
|
||||
assert.deepStrictEqual(JSON.stringify(testObject.configurationModel.overrides), JSON.stringify([{ identifiers: ['x', 'y', 'z'], keys: ['a'], contents: { 'a': 1 } }]));
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
suite('ConfigurationModel', () => {
|
||||
|
||||
test('setValue for a key that has no sections and not defined', () => {
|
||||
@@ -190,7 +219,7 @@ suite('ConfigurationModel', () => {
|
||||
let result = base.merge(add);
|
||||
|
||||
assert.deepStrictEqual(result.contents, { 'a': { 'b': 2 } });
|
||||
assert.deepStrictEqual(result.overrides, [{ identifiers: ['c'], contents: { 'a': 2, 'b': 2 }, keys: ['a'] }]);
|
||||
assert.deepStrictEqual(result.overrides, [{ identifiers: ['c'], contents: { 'a': 2, 'b': 2 }, keys: ['a', 'b'] }]);
|
||||
assert.deepStrictEqual(result.override('c').contents, { 'a': 2, 'b': 2 });
|
||||
assert.deepStrictEqual(result.keys, ['a.b']);
|
||||
});
|
||||
@@ -236,6 +265,45 @@ suite('ConfigurationModel', () => {
|
||||
|
||||
assert.deepStrictEqual(testObject.override('b').contents, { 'a': 2, 'c': 1 });
|
||||
});
|
||||
|
||||
test('Test override when an override has multiple identifiers', () => {
|
||||
const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['x', 'y'], contents: { 'a': 2 }, keys: ['a'] }]);
|
||||
|
||||
let actual = testObject.override('x');
|
||||
assert.deepStrictEqual(actual.contents, { 'a': 2, 'c': 1 });
|
||||
assert.deepStrictEqual(actual.keys, ['a', 'c']);
|
||||
assert.deepStrictEqual(testObject.getKeysForOverrideIdentifier('x'), ['a']);
|
||||
|
||||
actual = testObject.override('y');
|
||||
assert.deepStrictEqual(actual.contents, { 'a': 2, 'c': 1 });
|
||||
assert.deepStrictEqual(actual.keys, ['a', 'c']);
|
||||
assert.deepStrictEqual(testObject.getKeysForOverrideIdentifier('y'), ['a']);
|
||||
});
|
||||
|
||||
test('Test override when an identifier is defined in multiple overrides', () => {
|
||||
const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['x'], contents: { 'a': 3, 'b': 1 }, keys: ['a', 'b'] }, { identifiers: ['x', 'y'], contents: { 'a': 2 }, keys: ['a'] }]);
|
||||
|
||||
const actual = testObject.override('x');
|
||||
assert.deepStrictEqual(actual.contents, { 'a': 3, 'c': 1, 'b': 1 });
|
||||
assert.deepStrictEqual(actual.keys, ['a', 'c']);
|
||||
|
||||
assert.deepStrictEqual(testObject.getKeysForOverrideIdentifier('x'), ['a', 'b']);
|
||||
});
|
||||
|
||||
test('Test merge when configuration models have multiple identifiers', () => {
|
||||
const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['y'], contents: { 'c': 1 }, keys: ['c'] }, { identifiers: ['x', 'y'], contents: { 'a': 2 }, keys: ['a'] }]);
|
||||
const target = new ConfigurationModel({ 'a': 2, 'b': 1 }, ['a', 'b'], [{ identifiers: ['x'], contents: { 'a': 3, 'b': 2 }, keys: ['a', 'b'] }, { identifiers: ['x', 'y'], contents: { 'b': 3 }, keys: ['b'] }]);
|
||||
|
||||
const actual = testObject.merge(target);
|
||||
|
||||
assert.deepStrictEqual(actual.contents, { 'a': 2, 'c': 1, 'b': 1 });
|
||||
assert.deepStrictEqual(actual.keys, ['a', 'c', 'b']);
|
||||
assert.deepStrictEqual(actual.overrides, [
|
||||
{ identifiers: ['y'], contents: { 'c': 1 }, keys: ['c'] },
|
||||
{ identifiers: ['x', 'y'], contents: { 'a': 2, 'b': 3 }, keys: ['a', 'b'] },
|
||||
{ identifiers: ['x'], contents: { 'a': 3, 'b': 2 }, keys: ['a', 'b'] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
suite('CustomConfigurationModel', () => {
|
||||
@@ -375,6 +443,43 @@ suite('CustomConfigurationModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
suite('CustomConfigurationModel', () => {
|
||||
|
||||
test('Default configuration model uses overrides', () => {
|
||||
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
|
||||
'id': 'a',
|
||||
'order': 1,
|
||||
'title': 'a',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'a': {
|
||||
'description': 'a',
|
||||
'type': 'boolean',
|
||||
'default': false,
|
||||
}
|
||||
}
|
||||
});
|
||||
assert.strictEqual(true, new DefaultConfigurationModel().getValue('a'));
|
||||
});
|
||||
|
||||
test('Default configuration model uses overrides', () => {
|
||||
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
|
||||
'id': 'a',
|
||||
'order': 1,
|
||||
'title': 'a',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'a': {
|
||||
'description': 'a',
|
||||
'type': 'boolean',
|
||||
'default': false,
|
||||
}
|
||||
}
|
||||
});
|
||||
assert.strictEqual(false, new DefaultConfigurationModel({ a: false }).getValue('a'));
|
||||
});
|
||||
});
|
||||
|
||||
suite('Configuration', () => {
|
||||
|
||||
test('Test inspect for overrideIdentifiers', () => {
|
||||
@@ -582,11 +687,14 @@ suite('ConfigurationChangeEvent', () => {
|
||||
'files.autoSave': 'off',
|
||||
'[markdown]': {
|
||||
'editor.wordWrap': 'off'
|
||||
},
|
||||
'[typescript][jsonc]': {
|
||||
'editor.lineNumbers': 'off'
|
||||
}
|
||||
}));
|
||||
let testObject = new ConfigurationChangeEvent(change, undefined, configuration);
|
||||
|
||||
assert.deepStrictEqual(testObject.affectedKeys, ['files.autoSave', '[markdown]', 'editor.wordWrap']);
|
||||
assert.deepStrictEqual(testObject.affectedKeys, ['files.autoSave', '[markdown]', '[typescript][jsonc]', 'editor.wordWrap', 'editor.lineNumbers']);
|
||||
|
||||
assert.ok(testObject.affectsConfiguration('files'));
|
||||
assert.ok(testObject.affectsConfiguration('files.autoSave'));
|
||||
@@ -598,8 +706,16 @@ suite('ConfigurationChangeEvent', () => {
|
||||
|
||||
assert.ok(testObject.affectsConfiguration('editor'));
|
||||
assert.ok(testObject.affectsConfiguration('editor.wordWrap'));
|
||||
assert.ok(testObject.affectsConfiguration('editor.lineNumbers'));
|
||||
assert.ok(testObject.affectsConfiguration('editor', { overrideIdentifier: 'markdown' }));
|
||||
assert.ok(testObject.affectsConfiguration('editor', { overrideIdentifier: 'jsonc' }));
|
||||
assert.ok(testObject.affectsConfiguration('editor', { overrideIdentifier: 'typescript' }));
|
||||
assert.ok(testObject.affectsConfiguration('editor.wordWrap', { overrideIdentifier: 'markdown' }));
|
||||
assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { overrideIdentifier: 'jsonc' }));
|
||||
assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { overrideIdentifier: 'typescript' }));
|
||||
assert.ok(!testObject.affectsConfiguration('editor.lineNumbers', { overrideIdentifier: 'markdown' }));
|
||||
assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { overrideIdentifier: 'typescript' }));
|
||||
assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { overrideIdentifier: 'jsonc' }));
|
||||
assert.ok(!testObject.affectsConfiguration('editor', { overrideIdentifier: 'json' }));
|
||||
assert.ok(!testObject.affectsConfiguration('editor.fontSize', { overrideIdentifier: 'markdown' }));
|
||||
|
||||
@@ -615,6 +731,10 @@ suite('ConfigurationChangeEvent', () => {
|
||||
'editor.fontSize': 12,
|
||||
'editor.wordWrap': 'off'
|
||||
},
|
||||
'[css][scss]': {
|
||||
'editor.lineNumbers': 'off',
|
||||
'css.lint.emptyRules': 'error'
|
||||
},
|
||||
'files.autoSave': 'off',
|
||||
}));
|
||||
const data = configuration.toData();
|
||||
@@ -624,11 +744,15 @@ suite('ConfigurationChangeEvent', () => {
|
||||
'editor.fontSize': 13,
|
||||
'editor.wordWrap': 'off'
|
||||
},
|
||||
'[css][scss]': {
|
||||
'editor.lineNumbers': 'relative',
|
||||
'css.lint.emptyRules': 'error'
|
||||
},
|
||||
'window.zoomLevel': 1,
|
||||
}));
|
||||
let testObject = new ConfigurationChangeEvent(change, { data }, configuration);
|
||||
|
||||
assert.deepStrictEqual(testObject.affectedKeys, ['window.zoomLevel', '[markdown]', 'workbench.editor.enablePreview', 'editor.fontSize']);
|
||||
assert.deepStrictEqual(testObject.affectedKeys, ['window.zoomLevel', '[markdown]', '[css][scss]', 'workbench.editor.enablePreview', 'editor.fontSize', 'editor.lineNumbers']);
|
||||
|
||||
assert.ok(!testObject.affectsConfiguration('files'));
|
||||
|
||||
@@ -637,10 +761,18 @@ suite('ConfigurationChangeEvent', () => {
|
||||
assert.ok(!testObject.affectsConfiguration('[markdown].editor.fontSize'));
|
||||
assert.ok(!testObject.affectsConfiguration('[markdown].editor.wordWrap'));
|
||||
assert.ok(!testObject.affectsConfiguration('[markdown].workbench'));
|
||||
assert.ok(testObject.affectsConfiguration('[css][scss]'));
|
||||
|
||||
assert.ok(testObject.affectsConfiguration('editor'));
|
||||
assert.ok(testObject.affectsConfiguration('editor', { overrideIdentifier: 'markdown' }));
|
||||
assert.ok(testObject.affectsConfiguration('editor', { overrideIdentifier: 'css' }));
|
||||
assert.ok(testObject.affectsConfiguration('editor', { overrideIdentifier: 'scss' }));
|
||||
assert.ok(testObject.affectsConfiguration('editor.fontSize', { overrideIdentifier: 'markdown' }));
|
||||
assert.ok(!testObject.affectsConfiguration('editor.fontSize', { overrideIdentifier: 'css' }));
|
||||
assert.ok(!testObject.affectsConfiguration('editor.fontSize', { overrideIdentifier: 'scss' }));
|
||||
assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { overrideIdentifier: 'scss' }));
|
||||
assert.ok(testObject.affectsConfiguration('editor.lineNumbers', { overrideIdentifier: 'css' }));
|
||||
assert.ok(!testObject.affectsConfiguration('editor.lineNumbers', { overrideIdentifier: 'markdown' }));
|
||||
assert.ok(!testObject.affectsConfiguration('editor.wordWrap'));
|
||||
assert.ok(!testObject.affectsConfiguration('editor.wordWrap', { overrideIdentifier: 'markdown' }));
|
||||
assert.ok(!testObject.affectsConfiguration('editor', { overrideIdentifier: 'json' }));
|
||||
|
||||
@@ -21,16 +21,16 @@ suite('ConfigurationRegistry', () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
configurationRegistry.registerDefaultConfigurations([{ 'config': { a: 1, b: 2 } }]);
|
||||
configurationRegistry.registerDefaultConfigurations([{ '[lang]': { a: 2, c: 3 } }]);
|
||||
configurationRegistry.registerDefaultConfigurations([{ overrides: { 'config': { a: 1, b: 2 } } }]);
|
||||
configurationRegistry.registerDefaultConfigurations([{ overrides: { '[lang]': { a: 2, c: 3 } } }]);
|
||||
|
||||
assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 1, b: 2 });
|
||||
assert.deepStrictEqual(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 } }]);
|
||||
configurationRegistry.registerDefaultConfigurations([{ overrides: { '[lang]': { a: 1, b: 2 } } }]);
|
||||
configurationRegistry.registerDefaultConfigurations([{ overrides: { '[lang]': { a: 2, c: 3 } } }]);
|
||||
|
||||
assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['[lang]'].default, { a: 2, b: 2, c: 3 });
|
||||
});
|
||||
@@ -45,8 +45,8 @@ suite('ConfigurationRegistry', () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
configurationRegistry.registerDefaultConfigurations([{ 'config': { a: 1, b: 2 } }]);
|
||||
configurationRegistry.registerDefaultConfigurations([{ 'config': { a: 2, c: 3 } }]);
|
||||
configurationRegistry.registerDefaultConfigurations([{ overrides: { 'config': { a: 1, b: 2 } } }]);
|
||||
configurationRegistry.registerDefaultConfigurations([{ overrides: { 'config': { a: 2, c: 3 } } }]);
|
||||
|
||||
assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, c: 3 });
|
||||
});
|
||||
|
||||
@@ -139,7 +139,7 @@ suite('ConfigurationService', () => {
|
||||
configuration: {
|
||||
service: {
|
||||
testSetting: string;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,16 @@
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { TernarySearchTree } from 'vs/base/common/map';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { getConfigurationKeys, getConfigurationValue, IConfigurationOverrides, IConfigurationService, IConfigurationValue, isConfigurationOverrides } from 'vs/platform/configuration/common/configuration';
|
||||
import { getConfigurationValue, IConfigurationChangeEvent, IConfigurationOverrides, IConfigurationService, IConfigurationValue, isConfigurationOverrides } from 'vs/platform/configuration/common/configuration';
|
||||
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
|
||||
export class TestConfigurationService implements IConfigurationService {
|
||||
public _serviceBrand: undefined;
|
||||
|
||||
private configuration: any;
|
||||
readonly onDidChangeConfiguration = new Emitter<any>().event;
|
||||
readonly onDidChangeConfigurationEmitter = new Emitter<IConfigurationChangeEvent>();
|
||||
readonly onDidChangeConfiguration = this.onDidChangeConfigurationEmitter.event;
|
||||
|
||||
constructor(configuration?: any) {
|
||||
this.configuration = configuration || Object.create(null);
|
||||
@@ -55,19 +58,25 @@ export class TestConfigurationService implements IConfigurationService {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
private overrideIdentifiers: Map<string, string[]> = new Map();
|
||||
public setOverrideIdentifiers(key: string, identifiers: string[]): void {
|
||||
this.overrideIdentifiers.set(key, identifiers);
|
||||
}
|
||||
|
||||
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)
|
||||
userValue: getConfigurationValue<T>(config, key),
|
||||
overrideIdentifiers: this.overrideIdentifiers.get(key)
|
||||
};
|
||||
}
|
||||
|
||||
public keys() {
|
||||
return {
|
||||
default: getConfigurationKeys(),
|
||||
default: Object.keys(Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties()),
|
||||
user: Object.keys(this.configuration),
|
||||
workspace: [],
|
||||
workspaceFolder: []
|
||||
|
||||
@@ -12,14 +12,13 @@ import { localize } from 'vs/nls';
|
||||
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ContextKeyExpression, ContextKeyInfo, IContext, IContextKey, IContextKeyChangeEvent, IContextKeyService, IContextKeyServiceTarget, IReadableSet, RawContextKey, SET_CONTEXT_COMMAND_ID } 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 _value: { [key: string]: any };
|
||||
protected _id: number;
|
||||
|
||||
constructor(id: number, parent: Context | null) {
|
||||
@@ -59,7 +58,7 @@ export class Context implements IContext {
|
||||
this._parent = parent;
|
||||
}
|
||||
|
||||
public collectAllValues(): { [key: string]: any; } {
|
||||
public collectAllValues(): { [key: string]: any } {
|
||||
let result = this._parent ? this._parent.collectAllValues() : Object.create(null);
|
||||
result = { ...result, ...this._value };
|
||||
delete result['_contextId'];
|
||||
@@ -87,7 +86,7 @@ class NullContext extends Context {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
override collectAllValues(): { [key: string]: any; } {
|
||||
override collectAllValues(): { [key: string]: any } {
|
||||
return Object.create(null);
|
||||
}
|
||||
}
|
||||
@@ -176,7 +175,7 @@ class ConfigAwareContextValuesContainer extends Context {
|
||||
return super.removeValue(key);
|
||||
}
|
||||
|
||||
override collectAllValues(): { [key: string]: any; } {
|
||||
override collectAllValues(): { [key: string]: any } {
|
||||
const result: { [key: string]: any } = Object.create(null);
|
||||
this._values.forEach((value, index) => result[index] = value);
|
||||
return { ...result, ...super.collectAllValues() };
|
||||
@@ -300,7 +299,7 @@ export abstract class AbstractContextKeyService implements IContextKeyService {
|
||||
throw new Error(`AbstractContextKeyService has been disposed`);
|
||||
}
|
||||
const context = this.getContextValuesContainer(this._myContextId);
|
||||
const result = KeybindingResolver.contextMatchesRules(context, rules);
|
||||
const result = (rules ? rules.evaluate(context) : true);
|
||||
// console.group(rules.serialize() + ' -> ' + result);
|
||||
// rules.keys().forEach(key => { console.log(key, ctx[key]); });
|
||||
// console.groupEnd();
|
||||
@@ -536,7 +535,7 @@ class OverlayContextKeyService implements IContextKeyService {
|
||||
|
||||
contextMatchesRules(rules: ContextKeyExpression | undefined): boolean {
|
||||
const context = this.getContextValuesContainer(this.contextId);
|
||||
const result = KeybindingResolver.contextMatchesRules(context, rules);
|
||||
const result = (rules ? rules.evaluate(context) : true);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { isLinux, isMacintosh, isWeb, isWindows, userAgent } from 'vs/base/common/platform';
|
||||
import { isChrome, isEdge, isFirefox, isLinux, isMacintosh, isSafari, isWeb, isWindows } from 'vs/base/common/platform';
|
||||
import { isFalsyOrWhitespace } from 'vs/base/common/strings';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
let _userAgent = userAgent || '';
|
||||
const CONSTANT_VALUES = new Map<string, boolean>();
|
||||
CONSTANT_VALUES.set('false', false);
|
||||
CONSTANT_VALUES.set('true', true);
|
||||
@@ -17,10 +16,10 @@ CONSTANT_VALUES.set('isLinux', isLinux);
|
||||
CONSTANT_VALUES.set('isWindows', isWindows);
|
||||
CONSTANT_VALUES.set('isWeb', isWeb);
|
||||
CONSTANT_VALUES.set('isMacNative', isMacintosh && !isWeb);
|
||||
CONSTANT_VALUES.set('isEdge', _userAgent.indexOf('Edg/') >= 0);
|
||||
CONSTANT_VALUES.set('isFirefox', _userAgent.indexOf('Firefox') >= 0);
|
||||
CONSTANT_VALUES.set('isChrome', _userAgent.indexOf('Chrome') >= 0);
|
||||
CONSTANT_VALUES.set('isSafari', _userAgent.indexOf('Safari') >= 0);
|
||||
CONSTANT_VALUES.set('isEdge', isEdge);
|
||||
CONSTANT_VALUES.set('isFirefox', isFirefox);
|
||||
CONSTANT_VALUES.set('isChrome', isChrome);
|
||||
CONSTANT_VALUES.set('isSafari', isSafari);
|
||||
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
|
||||
@@ -1535,7 +1534,7 @@ export class RawContextKey<T> extends ContextKeyDefinedExpr {
|
||||
|
||||
private readonly _defaultValue: T | undefined;
|
||||
|
||||
constructor(key: string, defaultValue: T | undefined, metaOrHide?: string | true | { type: string, description: string }) {
|
||||
constructor(key: string, defaultValue: T | undefined, metaOrHide?: string | true | { type: string; description: string }) {
|
||||
super(key, null);
|
||||
this._defaultValue = defaultValue;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export const IsWindowsContext = new RawContextKey<boolean>('isWindows', isWindow
|
||||
|
||||
export const IsWebContext = new RawContextKey<boolean>('isWeb', isWeb, localize('isWeb', "Whether the platform is a web browser"));
|
||||
export const IsMacNativeContext = new RawContextKey<boolean>('isMacNative', isMacintosh && !isWeb, localize('isMacNative', "Whether the operating system is macOS on a non-browser platform"));
|
||||
export const IsIOSContext = new RawContextKey<boolean>('isIOS', isIOS, localize('isIOS', "Whether the operating system is IOS"));
|
||||
export const IsIOSContext = new RawContextKey<boolean>('isIOS', isIOS, localize('isIOS', "Whether the operating system is iOS"));
|
||||
|
||||
export const IsDevelopmentContext = new RawContextKey<boolean>('isDevelopment', false, true);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { $, addDisposableListener, EventType, isHTMLElement } from 'vs/base/brow
|
||||
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { Menu } from 'vs/base/browser/ui/menu/menu';
|
||||
import { ActionRunner, IRunEvent, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions';
|
||||
import { isPromiseCanceledError } from 'vs/base/common/errors';
|
||||
import { isCancellationError } from 'vs/base/common/errors';
|
||||
import { combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import 'vs/css!./contextMenuHandler';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
@@ -158,7 +158,7 @@ export class ContextMenuHandler {
|
||||
}
|
||||
|
||||
private onDidActionRun(e: IRunEvent): void {
|
||||
if (e.error && !isPromiseCanceledError(e.error)) {
|
||||
if (e.error && !isCancellationError(e.error)) {
|
||||
this.notificationService.error(e.error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,11 @@ export class ContextMenuService extends Disposable implements IContextMenuServic
|
||||
|
||||
private contextMenuHandler: ContextMenuHandler;
|
||||
|
||||
readonly onDidShowContextMenu = new Emitter<void>().event;
|
||||
private readonly _onDidShowContextMenu = new Emitter<void>();
|
||||
readonly onDidShowContextMenu = this._onDidShowContextMenu.event;
|
||||
|
||||
private readonly _onDidHideContextMenu = new Emitter<void>();
|
||||
readonly onDidHideContextMenu = this._onDidHideContextMenu.event;
|
||||
|
||||
constructor(
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@@ -40,7 +44,17 @@ export class ContextMenuService extends Disposable implements IContextMenuServic
|
||||
// ContextMenu
|
||||
|
||||
showContextMenu(delegate: IContextMenuDelegate): void {
|
||||
this.contextMenuHandler.showContextMenu(delegate);
|
||||
this.contextMenuHandler.showContextMenu({
|
||||
...delegate,
|
||||
onHide: (didCancel) => {
|
||||
if (delegate.onHide) {
|
||||
delegate.onHide(didCancel);
|
||||
}
|
||||
|
||||
this._onDidHideContextMenu.fire();
|
||||
}
|
||||
});
|
||||
ModifierKeyEmitter.getInstance().resetKeyStatus();
|
||||
this._onDidShowContextMenu.fire();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface IContextViewDelegate {
|
||||
|
||||
canRelayout?: boolean; // Default: true
|
||||
|
||||
getAnchor(): HTMLElement | { x: number; y: number; width?: number; height?: number; };
|
||||
getAnchor(): HTMLElement | { x: number; y: number; width?: number; height?: number };
|
||||
render(container: HTMLElement): IDisposable;
|
||||
onDOMEvent?(e: any, activeElement: HTMLElement): void;
|
||||
onHide?(data?: any): void;
|
||||
@@ -42,6 +42,7 @@ export interface IContextMenuService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly onDidShowContextMenu: Event<void>;
|
||||
readonly onDidHideContextMenu: Event<void>;
|
||||
|
||||
showContextMenu(delegate: IContextMenuDelegate): void;
|
||||
}
|
||||
|
||||
@@ -13,14 +13,14 @@ export class ContextViewService extends Disposable implements IContextViewServic
|
||||
|
||||
private currentViewDisposable: IDisposable = Disposable.None;
|
||||
private contextView: ContextView;
|
||||
private container: HTMLElement;
|
||||
private container: HTMLElement | null;
|
||||
|
||||
constructor(
|
||||
@ILayoutService readonly layoutService: ILayoutService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.container = layoutService.container;
|
||||
this.container = layoutService.hasContainer ? layoutService.container : null;
|
||||
this.contextView = this._register(new ContextView(this.container, ContextViewDOMPosition.ABSOLUTE));
|
||||
this.layout();
|
||||
|
||||
@@ -29,7 +29,7 @@ export class ContextViewService extends Disposable implements IContextViewServic
|
||||
|
||||
// ContextView
|
||||
|
||||
setContainer(container: HTMLElement, domPosition?: ContextViewDOMPosition): void {
|
||||
private setContainer(container: HTMLElement, domPosition?: ContextViewDOMPosition): void {
|
||||
this.contextView.setContainer(container, domPosition || ContextViewDOMPosition.ABSOLUTE);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export class ContextViewService extends Disposable implements IContextViewServic
|
||||
this.setContainer(container, shadowRoot ? ContextViewDOMPosition.FIXED_SHADOW : ContextViewDOMPosition.FIXED);
|
||||
}
|
||||
} else {
|
||||
if (this.container !== this.layoutService.container) {
|
||||
if (this.layoutService.hasContainer && this.container !== this.layoutService.container) {
|
||||
this.container = this.layoutService.container;
|
||||
this.setContainer(this.container, ContextViewDOMPosition.ABSOLUTE);
|
||||
}
|
||||
|
||||
84
src/vs/platform/credentials/common/credentials.ts
Normal file
84
src/vs/platform/credentials/common/credentials.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export const ICredentialsService = createDecorator<ICredentialsService>('credentialsService');
|
||||
|
||||
export interface ICredentialsProvider {
|
||||
getPassword(service: string, account: string): Promise<string | null>;
|
||||
setPassword(service: string, account: string, password: string): Promise<void>;
|
||||
deletePassword(service: string, account: string): Promise<boolean>;
|
||||
findPassword(service: string): Promise<string | null>;
|
||||
findCredentials(service: string): Promise<Array<{ account: string; password: string }>>;
|
||||
clear?(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ICredentialsChangeEvent {
|
||||
service: string;
|
||||
account: string;
|
||||
}
|
||||
|
||||
export interface ICredentialsService extends ICredentialsProvider {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly onDidChangePassword: Event<ICredentialsChangeEvent>;
|
||||
|
||||
/*
|
||||
* Each CredentialsService must provide a prefix that will be used
|
||||
* by the SecretStorage API when storing secrets.
|
||||
* This is a method that returns a Promise so that it can be defined in
|
||||
* the main process and proxied on the renderer side.
|
||||
*/
|
||||
getSecretStoragePrefix(): Promise<string>;
|
||||
}
|
||||
|
||||
export const ICredentialsMainService = createDecorator<ICredentialsMainService>('credentialsMainService');
|
||||
|
||||
export interface ICredentialsMainService extends ICredentialsService { }
|
||||
|
||||
interface ISecretVault {
|
||||
[service: string]: { [account: string]: string } | undefined;
|
||||
}
|
||||
|
||||
export class InMemoryCredentialsProvider implements ICredentialsProvider {
|
||||
private secretVault: ISecretVault = {};
|
||||
|
||||
async getPassword(service: string, account: string): Promise<string | null> {
|
||||
return this.secretVault[service]?.[account] ?? null;
|
||||
}
|
||||
|
||||
async setPassword(service: string, account: string, password: string): Promise<void> {
|
||||
this.secretVault[service] = this.secretVault[service] ?? {};
|
||||
this.secretVault[service]![account] = password;
|
||||
}
|
||||
|
||||
async deletePassword(service: string, account: string): Promise<boolean> {
|
||||
if (!this.secretVault[service]?.[account]) {
|
||||
return false;
|
||||
}
|
||||
delete this.secretVault[service]![account];
|
||||
if (Object.keys(this.secretVault[service]!).length === 0) {
|
||||
delete this.secretVault[service];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async findPassword(service: string): Promise<string | null> {
|
||||
return JSON.stringify(this.secretVault[service]) ?? null;
|
||||
}
|
||||
|
||||
async findCredentials(service: string): Promise<Array<{ account: string; password: string }>> {
|
||||
const credentials: { account: string; password: string }[] = [];
|
||||
for (const account of Object.keys(this.secretVault[service] || {})) {
|
||||
credentials.push({ account, password: this.secretVault[service]![account] });
|
||||
}
|
||||
return credentials;
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
this.secretVault = {};
|
||||
}
|
||||
}
|
||||
209
src/vs/platform/credentials/common/credentialsMainService.ts
Normal file
209
src/vs/platform/credentials/common/credentialsMainService.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ICredentialsChangeEvent, ICredentialsMainService, InMemoryCredentialsProvider } from 'vs/platform/credentials/common/credentials';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
|
||||
interface ChunkedPassword {
|
||||
content: string;
|
||||
hasNextChunk: boolean;
|
||||
}
|
||||
|
||||
export type KeytarModule = typeof import('keytar');
|
||||
|
||||
export abstract class BaseCredentialsMainService extends Disposable implements ICredentialsMainService {
|
||||
|
||||
private static readonly MAX_PASSWORD_LENGTH = 2500;
|
||||
private static readonly PASSWORD_CHUNK_SIZE = BaseCredentialsMainService.MAX_PASSWORD_LENGTH - 100;
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private _onDidChangePassword: Emitter<ICredentialsChangeEvent> = this._register(new Emitter());
|
||||
readonly onDidChangePassword = this._onDidChangePassword.event;
|
||||
|
||||
protected _keytarCache: KeytarModule | undefined;
|
||||
|
||||
constructor(
|
||||
@ILogService protected readonly logService: ILogService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
//#region abstract
|
||||
|
||||
public abstract getSecretStoragePrefix(): Promise<string>;
|
||||
protected abstract withKeytar(): Promise<KeytarModule>;
|
||||
/**
|
||||
* An optional method that subclasses can implement to assist in surfacing
|
||||
* Keytar load errors to the user in a friendly way.
|
||||
*/
|
||||
protected abstract surfaceKeytarLoadError?: (err: any) => void;
|
||||
|
||||
//#endregion
|
||||
|
||||
async getPassword(service: string, account: string): Promise<string | null> {
|
||||
let keytar: KeytarModule;
|
||||
try {
|
||||
keytar = await this.withKeytar();
|
||||
} catch (e) {
|
||||
// for get operations, we don't want to surface errors to the user
|
||||
return null;
|
||||
}
|
||||
|
||||
const password = await keytar.getPassword(service, account);
|
||||
if (password) {
|
||||
try {
|
||||
let { content, hasNextChunk }: ChunkedPassword = JSON.parse(password);
|
||||
if (!content || !hasNextChunk) {
|
||||
return password;
|
||||
}
|
||||
|
||||
let index = 1;
|
||||
while (hasNextChunk) {
|
||||
const nextChunk = await keytar.getPassword(service, `${account}-${index}`);
|
||||
const result: ChunkedPassword = JSON.parse(nextChunk!);
|
||||
content += result.content;
|
||||
hasNextChunk = result.hasNextChunk;
|
||||
index++;
|
||||
}
|
||||
|
||||
return content;
|
||||
} catch {
|
||||
return password;
|
||||
}
|
||||
}
|
||||
|
||||
return password;
|
||||
}
|
||||
|
||||
async setPassword(service: string, account: string, password: string): Promise<void> {
|
||||
let keytar: KeytarModule;
|
||||
try {
|
||||
keytar = await this.withKeytar();
|
||||
} catch (e) {
|
||||
this.surfaceKeytarLoadError?.(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
const MAX_SET_ATTEMPTS = 3;
|
||||
|
||||
// Sometimes Keytar has a problem talking to the keychain on the OS. To be more resilient, we retry a few times.
|
||||
const setPasswordWithRetry = async (service: string, account: string, password: string) => {
|
||||
let attempts = 0;
|
||||
let error: any;
|
||||
while (attempts < MAX_SET_ATTEMPTS) {
|
||||
try {
|
||||
await keytar.setPassword(service, account, password);
|
||||
return;
|
||||
} catch (e) {
|
||||
error = e;
|
||||
this.logService.warn('Error attempting to set a password: ', e?.message ?? e);
|
||||
attempts++;
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
}
|
||||
}
|
||||
|
||||
// throw last error
|
||||
throw error;
|
||||
};
|
||||
|
||||
if (isWindows && password.length > BaseCredentialsMainService.MAX_PASSWORD_LENGTH) {
|
||||
let index = 0;
|
||||
let chunk = 0;
|
||||
let hasNextChunk = true;
|
||||
while (hasNextChunk) {
|
||||
const passwordChunk = password.substring(index, index + BaseCredentialsMainService.PASSWORD_CHUNK_SIZE);
|
||||
index += BaseCredentialsMainService.PASSWORD_CHUNK_SIZE;
|
||||
hasNextChunk = password.length - index > 0;
|
||||
|
||||
const content: ChunkedPassword = {
|
||||
content: passwordChunk,
|
||||
hasNextChunk: hasNextChunk
|
||||
};
|
||||
|
||||
await setPasswordWithRetry(service, chunk ? `${account}-${chunk}` : account, JSON.stringify(content));
|
||||
chunk++;
|
||||
}
|
||||
|
||||
} else {
|
||||
await setPasswordWithRetry(service, account, password);
|
||||
}
|
||||
|
||||
this._onDidChangePassword.fire({ service, account });
|
||||
}
|
||||
|
||||
async deletePassword(service: string, account: string): Promise<boolean> {
|
||||
let keytar: KeytarModule;
|
||||
try {
|
||||
keytar = await this.withKeytar();
|
||||
} catch (e) {
|
||||
this.surfaceKeytarLoadError?.(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
const password = await keytar.getPassword(service, account);
|
||||
if (!password) {
|
||||
return false;
|
||||
}
|
||||
const didDelete = await keytar.deletePassword(service, account);
|
||||
let { content, hasNextChunk }: ChunkedPassword = JSON.parse(password);
|
||||
if (content && hasNextChunk) {
|
||||
// need to delete additional chunks
|
||||
let index = 1;
|
||||
while (hasNextChunk) {
|
||||
const accountWithIndex = `${account}-${index}`;
|
||||
const nextChunk = await keytar.getPassword(service, accountWithIndex);
|
||||
await keytar.deletePassword(service, accountWithIndex);
|
||||
|
||||
const result: ChunkedPassword = JSON.parse(nextChunk!);
|
||||
hasNextChunk = result.hasNextChunk;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
if (didDelete) {
|
||||
this._onDidChangePassword.fire({ service, account });
|
||||
}
|
||||
|
||||
return didDelete;
|
||||
}
|
||||
|
||||
async findPassword(service: string): Promise<string | null> {
|
||||
let keytar: KeytarModule;
|
||||
try {
|
||||
keytar = await this.withKeytar();
|
||||
} catch (e) {
|
||||
// for get operations, we don't want to surface errors to the user
|
||||
return null;
|
||||
}
|
||||
|
||||
return keytar.findPassword(service);
|
||||
}
|
||||
|
||||
async findCredentials(service: string): Promise<Array<{ account: string; password: string }>> {
|
||||
let keytar: KeytarModule;
|
||||
try {
|
||||
keytar = await this.withKeytar();
|
||||
} catch (e) {
|
||||
// for get operations, we don't want to surface errors to the user
|
||||
return [];
|
||||
}
|
||||
|
||||
return keytar.findCredentials(service);
|
||||
}
|
||||
|
||||
public clear(): Promise<void> {
|
||||
if (this._keytarCache instanceof InMemoryCredentialsProvider) {
|
||||
return this._keytarCache.clear();
|
||||
}
|
||||
|
||||
// We don't know how to properly clear Keytar because we don't know
|
||||
// what services have stored credentials. For reference, a "service" is an extension.
|
||||
// TODO: should we clear credentials for the built-in auth extensions?
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { InMemoryCredentialsProvider } from 'vs/platform/credentials/common/credentials';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
|
||||
import { BaseCredentialsMainService, KeytarModule } from 'vs/platform/credentials/common/credentialsMainService';
|
||||
|
||||
export class CredentialsNativeMainService extends BaseCredentialsMainService {
|
||||
|
||||
constructor(
|
||||
@ILogService logService: ILogService,
|
||||
@INativeEnvironmentService private readonly environmentMainService: INativeEnvironmentService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
|
||||
) {
|
||||
super(logService);
|
||||
}
|
||||
|
||||
// If the credentials service is running on the server, we add a suffix -server to differentiate from the location that the
|
||||
// client would store the credentials.
|
||||
public override async getSecretStoragePrefix() { return Promise.resolve(this.productService.urlProtocol); }
|
||||
|
||||
protected async withKeytar(): Promise<KeytarModule> {
|
||||
if (this._keytarCache) {
|
||||
return this._keytarCache;
|
||||
}
|
||||
|
||||
if (this.environmentMainService.disableKeytar) {
|
||||
this.logService.info('Keytar is disabled. Using in-memory credential store instead.');
|
||||
this._keytarCache = new InMemoryCredentialsProvider();
|
||||
return this._keytarCache;
|
||||
}
|
||||
|
||||
const keytarCache = await import('keytar');
|
||||
// Try using keytar to see if it throws or not.
|
||||
await keytarCache.findCredentials('test-keytar-loads');
|
||||
this._keytarCache = keytarCache;
|
||||
return this._keytarCache;
|
||||
}
|
||||
|
||||
protected override surfaceKeytarLoadError = (err: any) => {
|
||||
this.windowsMainService.sendToFocused('vscode:showCredentialsError', err.message ?? err);
|
||||
};
|
||||
}
|
||||
51
src/vs/platform/credentials/node/credentialsMainService.ts
Normal file
51
src/vs/platform/credentials/node/credentialsMainService.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { InMemoryCredentialsProvider } from 'vs/platform/credentials/common/credentials';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { BaseCredentialsMainService, KeytarModule } from 'vs/platform/credentials/common/credentialsMainService';
|
||||
|
||||
export class CredentialsWebMainService extends BaseCredentialsMainService {
|
||||
// Since we fallback to the in-memory credentials provider, we do not need to surface any Keytar load errors
|
||||
// to the user.
|
||||
protected surfaceKeytarLoadError?: (err: any) => void;
|
||||
|
||||
constructor(
|
||||
@ILogService logService: ILogService,
|
||||
@INativeEnvironmentService private readonly environmentMainService: INativeEnvironmentService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
) {
|
||||
super(logService);
|
||||
}
|
||||
|
||||
// If the credentials service is running on the server, we add a suffix -server to differentiate from the location that the
|
||||
// client would store the credentials.
|
||||
public override async getSecretStoragePrefix() { return Promise.resolve(`${this.productService.urlProtocol}-server`); }
|
||||
|
||||
protected async withKeytar(): Promise<KeytarModule> {
|
||||
if (this._keytarCache) {
|
||||
return this._keytarCache;
|
||||
}
|
||||
|
||||
if (this.environmentMainService.disableKeytar) {
|
||||
this.logService.info('Keytar is disabled. Using in-memory credential store instead.');
|
||||
this._keytarCache = new InMemoryCredentialsProvider();
|
||||
return this._keytarCache;
|
||||
}
|
||||
|
||||
try {
|
||||
this._keytarCache = await import('keytar');
|
||||
// Try using keytar to see if it throws or not.
|
||||
await this._keytarCache.findCredentials('test-keytar-loads');
|
||||
} catch (e) {
|
||||
this.logService.warn(
|
||||
`Using the in-memory credential store as the operating system's credential store could not be accessed. Please see https://aka.ms/vscode-server-keyring on how to set this up. Details: ${e.message ?? e}`);
|
||||
this._keytarCache = new InMemoryCredentialsProvider();
|
||||
}
|
||||
return this._keytarCache;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ export interface IDiagnosticsService {
|
||||
getPerformanceInfo(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<PerformanceInfo>;
|
||||
getSystemInfo(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<SystemInfo>;
|
||||
getDiagnostics(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<string>;
|
||||
getWorkspaceFileExtensions(workspace: IWorkspace): Promise<{ extensions: string[] }>;
|
||||
reportWorkspaceStats(workspace: IWorkspaceInformation): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -91,3 +92,34 @@ export interface IWorkspaceInformation extends IWorkspace {
|
||||
export function isRemoteDiagnosticError(x: any): x is IRemoteDiagnosticError {
|
||||
return !!x.hostName && !!x.errorMessage;
|
||||
}
|
||||
|
||||
export class NullDiagnosticsService implements IDiagnosticsService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
async getPerformanceInfo(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<PerformanceInfo> {
|
||||
return {};
|
||||
}
|
||||
|
||||
async getSystemInfo(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<SystemInfo> {
|
||||
return {
|
||||
processArgs: 'nullProcessArgs',
|
||||
gpuStatus: 'nullGpuStatus',
|
||||
screenReader: 'nullScreenReader',
|
||||
remoteData: [],
|
||||
os: 'nullOs',
|
||||
memory: 'nullMemory',
|
||||
vmHint: 'nullVmHint',
|
||||
};
|
||||
}
|
||||
|
||||
async getDiagnostics(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<string> {
|
||||
return '';
|
||||
}
|
||||
|
||||
async getWorkspaceFileExtensions(workspace: IWorkspace): Promise<{ extensions: string[] }> {
|
||||
return { extensions: [] };
|
||||
}
|
||||
|
||||
async reportWorkspaceStats(workspace: IWorkspaceInformation): Promise<void> { }
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event as IpcEvent } from 'electron';
|
||||
import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IDiagnosticInfo, IDiagnosticInfoOptions, IRemoteDiagnosticError, IRemoteDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ICodeWindow } from 'vs/platform/window/electron-main/window';
|
||||
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
|
||||
import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
|
||||
import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService';
|
||||
|
||||
export const ID = 'diagnosticsMainService';
|
||||
export const IDiagnosticsMainService = createDecorator<IDiagnosticsMainService>(ID);
|
||||
|
||||
export interface IRemoteDiagnosticOptions {
|
||||
includeProcesses?: boolean;
|
||||
includeWorkspaceMetadata?: boolean;
|
||||
}
|
||||
|
||||
export interface IDiagnosticsMainService {
|
||||
readonly _serviceBrand: undefined;
|
||||
getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]>;
|
||||
}
|
||||
|
||||
export class DiagnosticsMainService implements IDiagnosticsMainService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
|
||||
@IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService
|
||||
) { }
|
||||
|
||||
async getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]> {
|
||||
const windows = this.windowsMainService.getWindows();
|
||||
const diagnostics: Array<IDiagnosticInfo | IRemoteDiagnosticError | undefined> = await Promise.all(windows.map(window => {
|
||||
return new Promise<IDiagnosticInfo | IRemoteDiagnosticError | undefined>((resolve) => {
|
||||
const remoteAuthority = window.remoteAuthority;
|
||||
if (remoteAuthority) {
|
||||
const replyChannel = `vscode:getDiagnosticInfoResponse${window.id}`;
|
||||
const args: IDiagnosticInfoOptions = {
|
||||
includeProcesses: options.includeProcesses,
|
||||
folders: options.includeWorkspaceMetadata ? this.getFolderURIs(window) : undefined
|
||||
};
|
||||
|
||||
window.sendWhenReady('vscode:getDiagnosticInfo', CancellationToken.None, { replyChannel, args });
|
||||
|
||||
validatedIpcMain.once(replyChannel, (_: IpcEvent, data: IRemoteDiagnosticInfo) => {
|
||||
// No data is returned if getting the connection fails.
|
||||
if (!data) {
|
||||
resolve({ hostName: remoteAuthority, errorMessage: `Unable to resolve connection to '${remoteAuthority}'.` });
|
||||
}
|
||||
|
||||
resolve(data);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
resolve({ hostName: remoteAuthority, errorMessage: `Connection to '${remoteAuthority}' could not be established` });
|
||||
}, 5000);
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
return diagnostics.filter((x): x is IRemoteDiagnosticInfo | IRemoteDiagnosticError => !!x);
|
||||
}
|
||||
|
||||
private getFolderURIs(window: ICodeWindow): URI[] {
|
||||
const folderURIs: URI[] = [];
|
||||
|
||||
const workspace = window.openedWorkspace;
|
||||
if (isSingleFolderWorkspaceIdentifier(workspace)) {
|
||||
folderURIs.push(workspace.uri);
|
||||
} else if (isWorkspaceIdentifier(workspace)) {
|
||||
const resolvedWorkspace = this.workspacesManagementMainService.resolveLocalWorkspaceSync(workspace.configPath); // workspace folders can only be shown for local (resolved) workspaces
|
||||
if (resolvedWorkspace) {
|
||||
const rootFolders = resolvedWorkspace.folders;
|
||||
rootFolders.forEach(root => {
|
||||
folderURIs.push(root.uri);
|
||||
});
|
||||
} else {
|
||||
//TODO@RMacfarlane: can we add the workspace file here?
|
||||
}
|
||||
}
|
||||
|
||||
return folderURIs;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { ByteSize } from 'vs/platform/files/common/files';
|
||||
import { IMainProcessInfo } from 'vs/platform/launch/common/launch';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IWorkspace } from 'vs/platform/workspace/common/workspace';
|
||||
|
||||
export interface VersionInfo {
|
||||
vscodeVersion: string;
|
||||
@@ -38,7 +39,14 @@ interface ConfigFilePatterns {
|
||||
relativePathPattern?: RegExp;
|
||||
}
|
||||
|
||||
const worksapceStatsCache = new Map<string, Promise<WorkspaceStats>>();
|
||||
export async function collectWorkspaceStats(folder: string, filter: string[]): Promise<WorkspaceStats> {
|
||||
const cacheKey = `${folder}::${filter.join(':')}`;
|
||||
const cached = worksapceStatsCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const configFilePatterns: ConfigFilePatterns[] = [
|
||||
{ tag: 'grunt.js', filePattern: /^gruntfile\.js$/i },
|
||||
{ tag: 'gulp.js', filePattern: /^gulpfile\.js$/i },
|
||||
@@ -56,7 +64,7 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P
|
||||
{ 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 },
|
||||
{ tag: 'github-actions', filePattern: /^.+\.ya?ml$/i, relativePathPattern: /^\.github(?:\/|\\)workflows$/i },
|
||||
{ tag: 'devcontainer.json', filePattern: /^devcontainer\.json$/i },
|
||||
{ tag: 'dockerfile', filePattern: /^(dockerfile|docker\-compose\.ya?ml)$/i }
|
||||
];
|
||||
@@ -66,7 +74,7 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P
|
||||
|
||||
const MAX_FILES = 20000;
|
||||
|
||||
function collect(root: string, dir: string, filter: string[], token: { count: number, maxReached: boolean }): Promise<void> {
|
||||
function collect(root: string, dir: string, filter: string[], token: { count: number; maxReached: boolean }): Promise<void> {
|
||||
const relativePath = dir.substring(root.length + 1);
|
||||
|
||||
return Promises.withAsyncBody(async resolve => {
|
||||
@@ -135,17 +143,22 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P
|
||||
});
|
||||
}
|
||||
|
||||
const token: { count: number, maxReached: boolean } = { count: 0, maxReached: false };
|
||||
const statsPromise = Promises.withAsyncBody<WorkspaceStats>(async (resolve) => {
|
||||
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
|
||||
};
|
||||
await collect(folder, folder, filter, token);
|
||||
const launchConfigs = await collectLaunchConfigs(folder);
|
||||
resolve({
|
||||
configFiles: asSortedItems(configFiles),
|
||||
fileTypes: asSortedItems(fileTypes),
|
||||
fileCount: token.count,
|
||||
maxFilesReached: token.maxReached,
|
||||
launchConfigFiles: launchConfigs
|
||||
});
|
||||
});
|
||||
|
||||
worksapceStatsCache.set(cacheKey, statsPromise);
|
||||
return statsPromise;
|
||||
}
|
||||
|
||||
function asSortedItems(items: Map<string, number>): WorkspaceStatItem[] {
|
||||
@@ -496,6 +509,22 @@ export class DiagnosticsService implements IDiagnosticsService {
|
||||
}
|
||||
}
|
||||
|
||||
public async getWorkspaceFileExtensions(workspace: IWorkspace): Promise<{ extensions: string[] }> {
|
||||
const items = new Set<string>();
|
||||
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']);
|
||||
stats.fileTypes.forEach(item => items.add(item.name));
|
||||
} catch { }
|
||||
}
|
||||
return { extensions: [...items] };
|
||||
}
|
||||
|
||||
public async reportWorkspaceStats(workspace: IWorkspaceInformation): Promise<void> {
|
||||
for (const { uri } of workspace.folders) {
|
||||
const folderUri = URI.revive(uri);
|
||||
@@ -507,8 +536,8 @@ export class DiagnosticsService implements IDiagnosticsService {
|
||||
try {
|
||||
const stats = await collectWorkspaceStats(folder, ['node_modules', '.git']);
|
||||
type WorkspaceStatsClassification = {
|
||||
'workspace.id': { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
rendererSessionId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
'workspace.id': { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
|
||||
rendererSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
|
||||
};
|
||||
type WorkspaceStatsEvent = {
|
||||
'workspace.id': string | undefined;
|
||||
@@ -519,9 +548,9 @@ export class DiagnosticsService implements IDiagnosticsService {
|
||||
rendererSessionId: workspace.rendererSessionId
|
||||
});
|
||||
type WorkspaceStatsFileClassification = {
|
||||
rendererSessionId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
type: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
count: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
rendererSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
|
||||
type: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true };
|
||||
count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true };
|
||||
};
|
||||
type WorkspaceStatsFileEvent = {
|
||||
rendererSessionId: string;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { IMarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { basename } from 'vs/base/common/resources';
|
||||
@@ -13,49 +14,49 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
|
||||
import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
export interface FileFilter {
|
||||
extensions: string[];
|
||||
name: string;
|
||||
readonly extensions: string[];
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
export type DialogType = 'none' | 'info' | 'error' | 'question' | 'warning';
|
||||
|
||||
export interface ICheckbox {
|
||||
label: string;
|
||||
checked?: boolean;
|
||||
readonly label: string;
|
||||
readonly checked?: boolean;
|
||||
}
|
||||
|
||||
export interface IConfirmDialogArgs {
|
||||
confirmation: IConfirmation;
|
||||
readonly confirmation: IConfirmation;
|
||||
}
|
||||
|
||||
export interface IShowDialogArgs {
|
||||
severity: Severity;
|
||||
message: string;
|
||||
buttons?: string[];
|
||||
options?: IDialogOptions;
|
||||
readonly severity: Severity;
|
||||
readonly message: string;
|
||||
readonly buttons?: string[];
|
||||
readonly options?: IDialogOptions;
|
||||
}
|
||||
|
||||
export interface IInputDialogArgs extends IShowDialogArgs {
|
||||
buttons: string[];
|
||||
inputs: IInput[];
|
||||
readonly buttons: string[];
|
||||
readonly inputs: IInput[];
|
||||
}
|
||||
|
||||
export interface IDialog {
|
||||
confirmArgs?: IConfirmDialogArgs;
|
||||
showArgs?: IShowDialogArgs;
|
||||
inputArgs?: IInputDialogArgs;
|
||||
readonly confirmArgs?: IConfirmDialogArgs;
|
||||
readonly showArgs?: IShowDialogArgs;
|
||||
readonly inputArgs?: IInputDialogArgs;
|
||||
}
|
||||
|
||||
export type IDialogResult = IConfirmationResult | IInputResult | IShowResult;
|
||||
|
||||
export interface IConfirmation {
|
||||
title?: string;
|
||||
type?: DialogType;
|
||||
message: string;
|
||||
detail?: string;
|
||||
primaryButton?: string;
|
||||
secondaryButton?: string;
|
||||
checkbox?: ICheckbox;
|
||||
readonly title?: string;
|
||||
readonly type?: DialogType;
|
||||
readonly message: string;
|
||||
readonly detail?: string;
|
||||
readonly primaryButton?: string;
|
||||
readonly secondaryButton?: string;
|
||||
readonly checkbox?: ICheckbox;
|
||||
}
|
||||
|
||||
export interface IConfirmationResult {
|
||||
@@ -64,13 +65,13 @@ export interface IConfirmationResult {
|
||||
* Will be true if the dialog was confirmed with the primary button
|
||||
* pressed.
|
||||
*/
|
||||
confirmed: boolean;
|
||||
readonly confirmed: boolean;
|
||||
|
||||
/**
|
||||
* This will only be defined if the confirmation was created
|
||||
* with the checkbox option defined.
|
||||
*/
|
||||
checkboxChecked?: boolean;
|
||||
readonly checkboxChecked?: boolean;
|
||||
}
|
||||
|
||||
export interface IShowResult {
|
||||
@@ -80,13 +81,13 @@ export interface IShowResult {
|
||||
* 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;
|
||||
readonly choice: number;
|
||||
|
||||
/**
|
||||
* This will only be defined if the confirmation was created
|
||||
* with the checkbox option defined.
|
||||
*/
|
||||
checkboxChecked?: boolean;
|
||||
readonly checkboxChecked?: boolean;
|
||||
}
|
||||
|
||||
export interface IInputResult extends IShowResult {
|
||||
@@ -95,7 +96,7 @@ export interface IInputResult extends IShowResult {
|
||||
* Values for the input fields as provided by the user
|
||||
* or `undefined` if none.
|
||||
*/
|
||||
values?: string[];
|
||||
readonly values?: string[];
|
||||
}
|
||||
|
||||
export interface IPickAndOpenOptions {
|
||||
@@ -107,6 +108,7 @@ export interface IPickAndOpenOptions {
|
||||
}
|
||||
|
||||
export interface ISaveDialogOptions {
|
||||
|
||||
/**
|
||||
* A human-readable string for the dialog title
|
||||
*/
|
||||
@@ -136,6 +138,7 @@ export interface ISaveDialogOptions {
|
||||
}
|
||||
|
||||
export interface IOpenDialogOptions {
|
||||
|
||||
/**
|
||||
* A human-readable string for the dialog title
|
||||
*/
|
||||
@@ -182,35 +185,36 @@ export interface IOpenDialogOptions {
|
||||
export const IDialogService = createDecorator<IDialogService>('dialogService');
|
||||
|
||||
export interface ICustomDialogOptions {
|
||||
buttonDetails?: string[];
|
||||
markdownDetails?: ICustomDialogMarkdown[];
|
||||
classes?: string[];
|
||||
icon?: Codicon;
|
||||
disableCloseAction?: boolean;
|
||||
readonly buttonDetails?: string[];
|
||||
readonly markdownDetails?: ICustomDialogMarkdown[];
|
||||
readonly classes?: string[];
|
||||
readonly icon?: Codicon;
|
||||
readonly disableCloseAction?: boolean;
|
||||
}
|
||||
|
||||
export interface ICustomDialogMarkdown {
|
||||
markdown: IMarkdownString,
|
||||
classes?: string[]
|
||||
readonly markdown: IMarkdownString;
|
||||
readonly classes?: string[];
|
||||
}
|
||||
|
||||
export interface IDialogOptions {
|
||||
cancelId?: number;
|
||||
detail?: string;
|
||||
checkbox?: ICheckbox;
|
||||
custom?: boolean | ICustomDialogOptions;
|
||||
readonly cancelId?: number;
|
||||
readonly detail?: string;
|
||||
readonly checkbox?: ICheckbox;
|
||||
readonly custom?: boolean | ICustomDialogOptions;
|
||||
}
|
||||
|
||||
export interface IInput {
|
||||
placeholder?: string;
|
||||
type?: 'text' | 'password'
|
||||
value?: string;
|
||||
readonly placeholder?: string;
|
||||
readonly type?: 'text' | 'password';
|
||||
readonly value?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A handler to bring up modal dialogs.
|
||||
*/
|
||||
export interface IDialogHandler {
|
||||
|
||||
/**
|
||||
* Ask the user for confirmation with a modal dialog.
|
||||
*/
|
||||
@@ -251,6 +255,16 @@ export interface IDialogService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/**
|
||||
* An event that fires when a dialog is about to show.
|
||||
*/
|
||||
onWillShowDialog: Event<void>;
|
||||
|
||||
/**
|
||||
* An event that fires when a dialog did show (closed).
|
||||
*/
|
||||
onDidShowDialog: Event<void>;
|
||||
|
||||
/**
|
||||
* Ask the user for confirmation with a modal dialog.
|
||||
*/
|
||||
@@ -309,7 +323,7 @@ export interface IFileDialogService {
|
||||
* @param schemeFilter The scheme of the workspace path. If no filter given, the scheme of the current window is used.
|
||||
* Falls back to user home in the absence of enough information to find a better URI.
|
||||
*/
|
||||
defaultWorkspacePath(schemeFilter?: string, filename?: string): Promise<URI>;
|
||||
defaultWorkspacePath(schemeFilter?: string): Promise<URI>;
|
||||
|
||||
/**
|
||||
* Shows a file-folder selection dialog and opens the selected entry.
|
||||
@@ -376,10 +390,10 @@ export function getFileNamesMessage(fileNamesOrResources: readonly (string | URI
|
||||
}
|
||||
|
||||
export interface INativeOpenDialogOptions {
|
||||
forceNewWindow?: boolean;
|
||||
readonly forceNewWindow?: boolean;
|
||||
|
||||
defaultPath?: string;
|
||||
readonly defaultPath?: string;
|
||||
|
||||
telemetryEventName?: string;
|
||||
telemetryExtraData?: ITelemetryData;
|
||||
readonly telemetryEventName?: string;
|
||||
readonly telemetryExtraData?: ITelemetryData;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { hash } from 'vs/base/common/hash';
|
||||
import { mnemonicButtonLabel } from 'vs/base/common/labels';
|
||||
import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { normalizeNFC } from 'vs/base/common/normalization';
|
||||
import { dirname } from 'vs/base/common/path';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
import { Promises } from 'vs/base/node/pfs';
|
||||
@@ -17,8 +16,7 @@ import { localize } from 'vs/nls';
|
||||
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IStateMainService } from 'vs/platform/state/electron-main/state';
|
||||
import { WORKSPACE_FILTER } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { WORKSPACE_FILTER } from 'vs/platform/workspace/common/workspace';
|
||||
|
||||
export const IDialogMainService = createDecorator<IDialogMainService>('dialogMainService');
|
||||
|
||||
@@ -37,26 +35,23 @@ export interface IDialogMainService {
|
||||
}
|
||||
|
||||
interface IInternalNativeOpenDialogOptions extends INativeOpenDialogOptions {
|
||||
pickFolders?: boolean;
|
||||
pickFiles?: boolean;
|
||||
readonly pickFolders?: boolean;
|
||||
readonly pickFiles?: boolean;
|
||||
|
||||
title: string;
|
||||
buttonLabel?: string;
|
||||
filters?: FileFilter[];
|
||||
readonly title: string;
|
||||
readonly buttonLabel?: string;
|
||||
readonly filters?: FileFilter[];
|
||||
}
|
||||
|
||||
export class DialogMainService implements IDialogMainService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private static readonly workingDirPickerStorageKey = 'pickerWorkingDir';
|
||||
|
||||
private readonly windowFileDialogLocks = new Map<number, Set<number>>();
|
||||
private readonly windowDialogQueues = new Map<number, Queue<MessageBoxReturnValue | SaveDialogReturnValue | OpenDialogReturnValue>>();
|
||||
private readonly noWindowDialogueQueue = new Queue<MessageBoxReturnValue | SaveDialogReturnValue | OpenDialogReturnValue>();
|
||||
|
||||
constructor(
|
||||
@IStateMainService private readonly stateMainService: IStateMainService,
|
||||
@ILogService private readonly logService: ILogService
|
||||
) {
|
||||
}
|
||||
@@ -90,9 +85,6 @@ export class DialogMainService implements IDialogMainService {
|
||||
filters: options.filters
|
||||
};
|
||||
|
||||
// Ensure defaultPath
|
||||
dialogOptions.defaultPath = options.defaultPath || this.stateMainService.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
|
||||
@@ -111,18 +103,12 @@ export class DialogMainService implements IDialogMainService {
|
||||
}
|
||||
|
||||
// Show Dialog
|
||||
const windowToUse = window || BrowserWindow.getFocusedWindow();
|
||||
|
||||
const result = await this.showOpenDialog(dialogOptions, withNullAsUndefined(windowToUse));
|
||||
const result = await this.showOpenDialog(dialogOptions, withNullAsUndefined(window || BrowserWindow.getFocusedWindow()));
|
||||
if (result && result.filePaths && result.filePaths.length > 0) {
|
||||
|
||||
// Remember path in storage for next time
|
||||
this.stateMainService.setItem(DialogMainService.workingDirPickerStorageKey, dirname(result.filePaths[0]));
|
||||
|
||||
return result.filePaths;
|
||||
}
|
||||
|
||||
return undefined; // {{SQL CARBON EDIT}} Strict nulls
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getWindowDialogQueue<T extends MessageBoxReturnValue | SaveDialogReturnValue | OpenDialogReturnValue>(window?: BrowserWindow): Queue<T> {
|
||||
@@ -154,7 +140,7 @@ export class DialogMainService implements IDialogMainService {
|
||||
|
||||
async showSaveDialog(options: SaveDialogOptions, window?: BrowserWindow): Promise<SaveDialogReturnValue> {
|
||||
|
||||
// prevent duplicates of the same dialog queueing at the same time
|
||||
// Prevent duplicates of the same dialog queueing at the same time
|
||||
const fileDialogLock = this.acquireFileDialogLock(options, window);
|
||||
if (!fileDialogLock) {
|
||||
this.logService.error('[DialogMainService]: file save dialog is already or will be showing for the window with the same configuration');
|
||||
@@ -204,7 +190,7 @@ export class DialogMainService implements IDialogMainService {
|
||||
}
|
||||
}
|
||||
|
||||
// prevent duplicates of the same dialog queueing at the same time
|
||||
// Prevent duplicates of the same dialog queueing at the same time
|
||||
const fileDialogLock = this.acquireFileDialogLock(options, window);
|
||||
if (!fileDialogLock) {
|
||||
this.logService.error('[DialogMainService]: file open dialog is already or will be showing for the window with the same configuration');
|
||||
@@ -232,19 +218,21 @@ export class DialogMainService implements IDialogMainService {
|
||||
|
||||
private acquireFileDialogLock(options: SaveDialogOptions | OpenDialogOptions, window?: BrowserWindow): IDisposable | undefined {
|
||||
|
||||
// if no window is provided, allow as many dialogs as
|
||||
// If no window is provided, allow as many dialogs as
|
||||
// needed since we consider them not modal per window
|
||||
if (!window) {
|
||||
return Disposable.None;
|
||||
}
|
||||
|
||||
// if a window is provided, only allow a single dialog
|
||||
// If a window is provided, only allow a single dialog
|
||||
// at the same time because dialogs are modal and we
|
||||
// do not want to open one dialog after the other
|
||||
// (https://github.com/microsoft/vscode/issues/114432)
|
||||
// we figure this out by `hashing` the configuration
|
||||
// options for the dialog to prevent duplicates
|
||||
|
||||
this.logService.trace('[DialogMainService]: request to acquire file dialog lock', options);
|
||||
|
||||
let windowFileDialogLocks = this.windowFileDialogLocks.get(window.id);
|
||||
if (!windowFileDialogLocks) {
|
||||
windowFileDialogLocks = new Set();
|
||||
@@ -256,12 +244,16 @@ export class DialogMainService implements IDialogMainService {
|
||||
return undefined; // prevent duplicates, return
|
||||
}
|
||||
|
||||
this.logService.trace('[DialogMainService]: new file dialog lock created', options);
|
||||
|
||||
windowFileDialogLocks.add(optionsHash);
|
||||
|
||||
return toDisposable(() => {
|
||||
this.logService.trace('[DialogMainService]: file dialog lock disposed', options);
|
||||
|
||||
windowFileDialogLocks?.delete(optionsHash);
|
||||
|
||||
// if the window has no more dialog locks, delete it from the set of locks
|
||||
// If the window has no more dialog locks, delete it from the set of locks
|
||||
if (windowFileDialogLocks?.size === 0) {
|
||||
this.windowFileDialogLocks.delete(window.id);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { IConfirmation, IConfirmationResult, IDialogOptions, IDialogService, IInputResult, IShowResult } from 'vs/platform/dialogs/common/dialogs';
|
||||
|
||||
@@ -10,6 +11,9 @@ export class TestDialogService implements IDialogService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
readonly onWillShowDialog = Event.None;
|
||||
readonly onDidShowDialog = Event.None;
|
||||
|
||||
private confirmResult: IConfirmationResult | undefined = undefined;
|
||||
setConfirmResult(result: IConfirmationResult) {
|
||||
this.confirmResult = result;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Schemas } from 'vs/base/common/network';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IDownloadService } from 'vs/platform/download/common/download';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { asText, IRequestService } from 'vs/platform/request/common/request';
|
||||
import { asTextOrError, IRequestService } from 'vs/platform/request/common/request';
|
||||
|
||||
export class DownloadService implements IDownloadService {
|
||||
|
||||
@@ -30,7 +30,7 @@ export class DownloadService implements IDownloadService {
|
||||
if (context.res.statusCode === 200) {
|
||||
await this.fileService.writeFile(target, context.stream);
|
||||
} else {
|
||||
const message = await asText(context);
|
||||
const message = await asTextOrError(context);
|
||||
throw new Error(`Expected 200, got back ${context.res.statusCode} instead.\n\n${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { getClientArea, getTopLeftOffset } from 'vs/base/browser/dom';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { language, locale } from 'vs/base/common/platform';
|
||||
import { IElement, ILocaleInfo, ILocalizedStrings, IWindowDriver } from 'vs/platform/driver/common/driver';
|
||||
import localizedStrings from 'vs/platform/localizations/common/localizedStrings';
|
||||
|
||||
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 | HTMLSelectElement; // {{SQL CARBON EDIT}} handle select element
|
||||
inputElement.value = text;
|
||||
|
||||
const event = new Event(inputElement.tagName === 'INPUT' ? 'input' : 'change', { bubbles: true, cancelable: true }); // {{SQL CARBON EDIT}} handle select element
|
||||
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);
|
||||
}
|
||||
|
||||
getLocaleInfo(): Promise<ILocaleInfo> {
|
||||
return Promise.resolve({
|
||||
language: language,
|
||||
locale: locale
|
||||
});
|
||||
}
|
||||
|
||||
getLocalizedStrings(): Promise<ILocalizedStrings> {
|
||||
return Promise.resolve({
|
||||
open: localizedStrings.open,
|
||||
close: localizedStrings.close,
|
||||
find: localizedStrings.find
|
||||
});
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
||||
@@ -3,23 +3,214 @@
|
||||
* Licensed under the Source EULA. 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';
|
||||
import { getClientArea, getTopLeftOffset } from 'vs/base/browser/dom';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { language, locale } from 'vs/base/common/platform';
|
||||
import { IElement, ILocaleInfo, ILocalizedStrings, IWindowDriver } from 'vs/platform/driver/common/driver';
|
||||
import localizedStrings from 'vs/platform/localizations/common/localizedStrings';
|
||||
|
||||
export class BrowserWindowDriver implements IWindowDriver {
|
||||
|
||||
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(this.serializeElement(element, recursive));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private 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(this.serializeElement(child, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { left, top } = getTopLeftOffset(element as HTMLElement);
|
||||
|
||||
return {
|
||||
tagName: element.tagName,
|
||||
className: element.className,
|
||||
textContent: element.textContent || '',
|
||||
attributes,
|
||||
children,
|
||||
left,
|
||||
top
|
||||
};
|
||||
}
|
||||
|
||||
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.active.length; i++) {
|
||||
lines.push(xterm.buffer.active.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);
|
||||
}
|
||||
|
||||
getLocaleInfo(): Promise<ILocaleInfo> {
|
||||
return Promise.resolve({
|
||||
language: language,
|
||||
locale: locale
|
||||
});
|
||||
}
|
||||
|
||||
getLocalizedStrings(): Promise<ILocalizedStrings> {
|
||||
return Promise.resolve({
|
||||
open: localizedStrings.open,
|
||||
close: localizedStrings.close,
|
||||
find: localizedStrings.find
|
||||
});
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
click(selector: string, xoffset?: number, yoffset?: number): Promise<void> {
|
||||
|
||||
// This is actually not used in the playwright drivers
|
||||
// that can implement `click` natively via the driver
|
||||
|
||||
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.');
|
||||
|
||||
async exitApplication(): Promise<void> {
|
||||
// No-op in web
|
||||
}
|
||||
}
|
||||
|
||||
export async function registerWindowDriver(): Promise<IDisposable> {
|
||||
(<any>window).driver = new BrowserWindowDriver();
|
||||
|
||||
return Disposable.None;
|
||||
export function registerWindowDriver(): void {
|
||||
Object.assign(window, { driver: new BrowserWindowDriver() });
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
* Licensed under the Source EULA. 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
|
||||
@@ -12,21 +10,14 @@ export interface IElement {
|
||||
tagName: string;
|
||||
className: string;
|
||||
textContent: string;
|
||||
attributes: { [name: string]: string; };
|
||||
attributes: { [name: string]: string };
|
||||
children: IElement[];
|
||||
top: number;
|
||||
left: number;
|
||||
}
|
||||
|
||||
export interface ILocaleInfo {
|
||||
/**
|
||||
* The UI language used.
|
||||
*/
|
||||
language: string;
|
||||
|
||||
/**
|
||||
* The requested locale
|
||||
*/
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
@@ -36,52 +27,18 @@ export interface ILocalizedStrings {
|
||||
find: string;
|
||||
}
|
||||
|
||||
export interface IDriver {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
getWindowIds(): Promise<number[]>;
|
||||
capturePage(windowId: number): Promise<string>;
|
||||
reloadWindow(windowId: number): Promise<void>;
|
||||
exitApplication(): Promise<boolean>;
|
||||
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>;
|
||||
getLocaleInfo(windowId: number): Promise<ILocaleInfo>;
|
||||
getLocalizedStrings(windowId: number): Promise<ILocalizedStrings>;
|
||||
}
|
||||
//*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; }>;
|
||||
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>;
|
||||
getLocaleInfo(): Promise<ILocaleInfo>;
|
||||
getLocalizedStrings(): Promise<ILocalizedStrings>
|
||||
}
|
||||
|
||||
export interface IDriverOptions {
|
||||
verbose: boolean;
|
||||
}
|
||||
|
||||
export interface IWindowDriverRegistry {
|
||||
registerWindowDriver(windowId: number): Promise<IDriverOptions>;
|
||||
reloadWindowDriver(windowId: number): Promise<void>;
|
||||
getLocalizedStrings(): Promise<ILocalizedStrings>;
|
||||
exitApplication(): Promise<void>;
|
||||
}
|
||||
//*END
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IDriverOptions, IElement, ILocaleInfo, ILocalizedStrings as ILocalizedStrings, IWindowDriver, IWindowDriverRegistry } from 'vs/platform/driver/common/driver';
|
||||
|
||||
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]);
|
||||
case 'getLocaleInfo': return this.driver.getLocaleInfo();
|
||||
case 'getLocalizedStrings': return this.driver.getLocalizedStrings();
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
getLocaleInfo(): Promise<ILocaleInfo> {
|
||||
return this.channel.call('getLocaleInfo');
|
||||
}
|
||||
|
||||
getLocalizedStrings(): Promise<ILocalizedStrings> {
|
||||
return this.channel.call('getLocalizedStrings');
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { KeybindingParser } from 'vs/base/common/keybindingParser';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { SimpleKeybinding, ScanCodeBinding } from 'vs/base/common/keybindings';
|
||||
import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { OS } from 'vs/base/common/platform';
|
||||
import { IPCServer, StaticRouter } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { serve as serveNet } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { IDriver, IDriverOptions, IElement, ILocaleInfo, ILocalizedStrings, IWindowDriver, IWindowDriverRegistry } from 'vs/platform/driver/common/driver';
|
||||
import { WindowDriverChannelClient } from 'vs/platform/driver/common/driverIpc';
|
||||
import { DriverChannel, WindowDriverRegistryChannel } from 'vs/platform/driver/node/driver';
|
||||
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
|
||||
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
|
||||
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
|
||||
|
||||
function isSilentKeyCode(keyCode: KeyCode) {
|
||||
return keyCode < KeyCode.Digit0;
|
||||
}
|
||||
|
||||
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
|
||||
) { }
|
||||
|
||||
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?.win) {
|
||||
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);
|
||||
}
|
||||
|
||||
exitApplication(): Promise<boolean> {
|
||||
return this.lifecycleMainService.quit();
|
||||
}
|
||||
|
||||
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?.win) {
|
||||
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);
|
||||
}
|
||||
|
||||
async getLocaleInfo(windowId: number): Promise<ILocaleInfo> {
|
||||
const windowDriver = await this.getWindowDriver(windowId);
|
||||
return await windowDriver.getLocaleInfo();
|
||||
}
|
||||
|
||||
async getLocalizedStrings(windowId: number): Promise<ILocalizedStrings> {
|
||||
const windowDriver = await this.getWindowDriver(windowId);
|
||||
return await windowDriver.getLocalizedStrings();
|
||||
}
|
||||
|
||||
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,
|
||||
environmentMainService: IEnvironmentMainService,
|
||||
instantiationService: IInstantiationService
|
||||
): Promise<IDisposable> {
|
||||
const verbose = environmentMainService.driverVerbose;
|
||||
const driver = instantiationService.createInstance(Driver as any, windowServer, { verbose }) as Driver; // {{SQL CARBON EDIT}} strict-null-check...i guess?
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -3,63 +3,23 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { BaseWindowDriver } from 'vs/platform/driver/browser/baseDriver';
|
||||
import { WindowDriverChannel, WindowDriverRegistryChannelClient } from 'vs/platform/driver/common/driverIpc';
|
||||
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services';
|
||||
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
|
||||
import { BrowserWindowDriver } from 'vs/platform/driver/browser/driver';
|
||||
|
||||
class WindowDriver extends BaseWindowDriver {
|
||||
interface INativeWindowDriverHelper {
|
||||
exitApplication(): Promise<void>;
|
||||
}
|
||||
|
||||
constructor(
|
||||
@INativeHostService private readonly nativeHostService: INativeHostService
|
||||
) {
|
||||
class NativeWindowDriver extends BrowserWindowDriver {
|
||||
|
||||
constructor(private readonly helper: INativeWindowDriverHelper) {
|
||||
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' });
|
||||
override exitApplication(): Promise<void> {
|
||||
return this.helper.exitApplication();
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
export function registerWindowDriver(helper: INativeWindowDriverHelper): void {
|
||||
Object.assign(window, { driver: new NativeWindowDriver(helper) });
|
||||
}
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Client } from 'vs/base/parts/ipc/common/ipc.net';
|
||||
import { connect as connectNet } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { IDriver, IElement, ILocaleInfo, ILocalizedStrings, IWindowDriverRegistry } 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]);
|
||||
case 'getLocaleInfo': return this.driver.getLocaleInfo(arg);
|
||||
case 'getLocalizedStrings': return this.driver.getLocalizedStrings(arg);
|
||||
}
|
||||
|
||||
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<boolean> {
|
||||
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]);
|
||||
}
|
||||
|
||||
getLocaleInfo(windowId: number): Promise<ILocaleInfo> {
|
||||
return this.channel.call('getLocaleInfo', windowId);
|
||||
}
|
||||
|
||||
getLocalizedStrings(windowId: number): Promise<ILocalizedStrings> {
|
||||
return this.channel.call('getLocalizedStrings', windowId);
|
||||
}
|
||||
}
|
||||
|
||||
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 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 };
|
||||
}
|
||||
@@ -88,10 +88,10 @@ export interface IBaseTextResourceEditorInput extends IBaseResourceEditorInput {
|
||||
encoding?: string;
|
||||
|
||||
/**
|
||||
* The identifier of the language mode of the text input
|
||||
* The identifier of the language id of the text input
|
||||
* if known to use when displaying the contents.
|
||||
*/
|
||||
mode?: string;
|
||||
languageId?: string;
|
||||
}
|
||||
|
||||
export interface IResourceEditorInput extends IBaseResourceEditorInput {
|
||||
@@ -177,7 +177,7 @@ export enum EditorResolution {
|
||||
EXCLUSIVE_ONLY
|
||||
}
|
||||
|
||||
export enum EditorOpenContext {
|
||||
export enum EditorOpenSource {
|
||||
|
||||
/**
|
||||
* Default: the editor is opening via a programmatic call
|
||||
@@ -265,6 +265,15 @@ export interface IEditorOptions {
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* In case of an error opening the editor, will not present this error to the user (e.g. by showing
|
||||
* a generic placeholder in the editor area). So it is up to the caller to provide error information
|
||||
* in that case.
|
||||
*
|
||||
* By default, an error when opening an editor will result in a placeholder editor that shows the error.
|
||||
* In certain cases a modal dialog may be presented to ask the user for further action.
|
||||
*/
|
||||
ignoreError?: boolean;
|
||||
|
||||
/**
|
||||
@@ -278,18 +287,18 @@ export interface IEditorOptions {
|
||||
/**
|
||||
* A optional hint to signal in which context the editor opens.
|
||||
*
|
||||
* If configured to be `EditorOpenContext.USER`, this hint can be
|
||||
* If configured to be `EditorOpenSource.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.
|
||||
*/
|
||||
context?: EditorOpenContext;
|
||||
source?: EditorOpenSource;
|
||||
|
||||
/**
|
||||
* An optional property to signal that certain view state should be
|
||||
* applied when opening the editor.
|
||||
* applied when opening the editor.
|
||||
*/
|
||||
viewState?: object;
|
||||
}
|
||||
@@ -324,6 +333,31 @@ export const enum TextEditorSelectionRevealType {
|
||||
NearTopIfOutsideViewport = 3,
|
||||
}
|
||||
|
||||
export const enum TextEditorSelectionSource {
|
||||
|
||||
/**
|
||||
* Programmatic source indicates a selection change that
|
||||
* was not triggered by the user via keyboard or mouse
|
||||
* but through text editor APIs.
|
||||
*/
|
||||
PROGRAMMATIC = 'api',
|
||||
|
||||
/**
|
||||
* Navigation source indicates a selection change that
|
||||
* was caused via some command or UI component such as
|
||||
* an outline tree.
|
||||
*/
|
||||
NAVIGATION = 'code.navigation',
|
||||
|
||||
/**
|
||||
* Jump source indicates a selection change that
|
||||
* was caused from within the text editor to another
|
||||
* location in the same or different text editor such
|
||||
* as "Go to definition".
|
||||
*/
|
||||
JUMP = 'code.jump'
|
||||
}
|
||||
|
||||
export interface ITextEditorOptions extends IEditorOptions {
|
||||
|
||||
/**
|
||||
@@ -336,4 +370,9 @@ export interface ITextEditorOptions extends IEditorOptions {
|
||||
* Defaults to TextEditorSelectionRevealType.Center
|
||||
*/
|
||||
selectionRevealType?: TextEditorSelectionRevealType;
|
||||
|
||||
/**
|
||||
* Source of the call that caused the selection.
|
||||
*/
|
||||
selectionSource?: TextEditorSelectionSource | string;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export const IEncryptionMainService = createDecorator<IEncryptionMainService>('encryptionMainService');
|
||||
|
||||
export interface IEncryptionMainService extends ICommonEncryptionService { }
|
||||
|
||||
export interface ICommonEncryptionService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ICommonEncryptionService } from 'vs/platform/encryption/common/encryptionService';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export const IEncryptionMainService = createDecorator<IEncryptionMainService>('encryptionMainService');
|
||||
|
||||
export interface IEncryptionMainService extends ICommonEncryptionService { }
|
||||
|
||||
export interface Encryption {
|
||||
encrypt(salt: string, value: string): Promise<string>;
|
||||
@@ -58,6 +58,7 @@ export interface NativeParsedArgs {
|
||||
'show-versions'?: boolean;
|
||||
'category'?: string;
|
||||
'install-extension'?: string[]; // undefined or array of 1 or more
|
||||
'pre-release'?: boolean;
|
||||
'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
|
||||
@@ -78,8 +79,7 @@ export interface NativeParsedArgs {
|
||||
'max-memory'?: string;
|
||||
'file-write'?: boolean;
|
||||
'file-chmod'?: boolean;
|
||||
'driver'?: string;
|
||||
'driver-verbose'?: boolean;
|
||||
'enable-smoke-test-driver'?: boolean;
|
||||
'remote'?: string;
|
||||
'force'?: boolean;
|
||||
'do-not-sync'?: boolean;
|
||||
@@ -115,4 +115,8 @@ export interface NativeParsedArgs {
|
||||
'allow-insecure-localhost'?: boolean;
|
||||
'log-net-log'?: string;
|
||||
'vmodule'?: string;
|
||||
'disable-dev-shm-usage'?: boolean;
|
||||
|
||||
// MS Build command line arg
|
||||
'ms-enable-electron-run-as-node'?: boolean;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
|
||||
import { ExtensionKind } from 'vs/platform/extensions/common/extensions';
|
||||
import { createDecorator, refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export const IEnvironmentService = createDecorator<IEnvironmentService>('environmentService');
|
||||
@@ -21,6 +20,13 @@ export interface IExtensionHostDebugParams extends IDebugParams {
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of extension.
|
||||
*
|
||||
* **NOTE**: This is defined in `platform/environment` because it can appear as a CLI argument.
|
||||
*/
|
||||
export type ExtensionKind = 'ui' | 'workspace' | 'web';
|
||||
|
||||
/**
|
||||
* A basic environment service that can be used in various processes,
|
||||
* such as main, renderer and shared process. Use subclasses of this
|
||||
@@ -53,6 +59,8 @@ export interface IEnvironmentService {
|
||||
untitledWorkspacesHome: URI;
|
||||
globalStorageHome: URI;
|
||||
workspaceStorageHome: URI;
|
||||
localHistoryHome: URI;
|
||||
cacheHome: URI;
|
||||
|
||||
// --- settings sync
|
||||
userDataSyncHome: URI;
|
||||
@@ -68,9 +76,6 @@ export interface IEnvironmentService {
|
||||
extensionDevelopmentKind?: ExtensionKind[];
|
||||
extensionTestsLocationURI?: URI;
|
||||
|
||||
// --- workspace trust
|
||||
disableWorkspaceTrust: boolean;
|
||||
|
||||
// --- logging
|
||||
logsPath: string;
|
||||
logLevel?: string;
|
||||
@@ -127,8 +132,8 @@ export interface INativeEnvironmentService extends IEnvironmentService {
|
||||
extensionsDownloadPath: string;
|
||||
builtinExtensionsPath: string;
|
||||
|
||||
// --- smoke test support
|
||||
driverHandle?: string;
|
||||
// --- use keytar for credentials
|
||||
disableKeytar?: boolean;
|
||||
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
//
|
||||
|
||||
@@ -11,8 +11,7 @@ import { env } from 'vs/base/common/process';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
|
||||
import { IDebugParams, IExtensionHostDebugParams, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { ExtensionKind } from 'vs/platform/extensions/common/extensions';
|
||||
import { ExtensionKind, IDebugParams, IExtensionHostDebugParams, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
|
||||
export interface INativeEnvironmentPaths {
|
||||
@@ -23,7 +22,7 @@ export interface INativeEnvironmentPaths {
|
||||
*
|
||||
* Only one instance of VSCode can use the same `userDataDir`.
|
||||
*/
|
||||
userDataDir: string
|
||||
userDataDir: string;
|
||||
|
||||
/**
|
||||
* The user home directory mainly used for persisting extensions
|
||||
@@ -35,7 +34,7 @@ export interface INativeEnvironmentPaths {
|
||||
/**
|
||||
* OS tmp dir.
|
||||
*/
|
||||
tmpDir: string,
|
||||
tmpDir: string;
|
||||
}
|
||||
|
||||
export abstract class AbstractNativeEnvironmentService implements INativeEnvironmentService {
|
||||
@@ -57,6 +56,9 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron
|
||||
@memoize
|
||||
get tmpDir(): URI { return URI.file(this.paths.tmpDir); }
|
||||
|
||||
@memoize
|
||||
get cacheHome(): URI { return URI.file(this.userDataPath); }
|
||||
|
||||
@memoize
|
||||
get userRoamingDataHome(): URI { return this.appSettingsHome; }
|
||||
|
||||
@@ -85,10 +87,13 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron
|
||||
get machineSettingsResource(): URI { return joinPath(URI.file(join(this.userDataPath, 'Machine')), 'settings.json'); }
|
||||
|
||||
@memoize
|
||||
get globalStorageHome(): URI { return URI.joinPath(this.appSettingsHome, 'globalStorage'); }
|
||||
get globalStorageHome(): URI { return joinPath(this.appSettingsHome, 'globalStorage'); }
|
||||
|
||||
@memoize
|
||||
get workspaceStorageHome(): URI { return URI.joinPath(this.appSettingsHome, 'workspaceStorage'); }
|
||||
get workspaceStorageHome(): URI { return joinPath(this.appSettingsHome, 'workspaceStorage'); }
|
||||
|
||||
@memoize
|
||||
get localHistoryHome(): URI { return joinPath(this.appSettingsHome, 'History'); }
|
||||
|
||||
@memoize
|
||||
get keybindingsResource(): URI { return joinPath(this.userRoamingDataHome, 'keybindings.json'); }
|
||||
@@ -225,8 +230,6 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron
|
||||
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(join(this.logsPath, 'telemetry.log')); }
|
||||
get disableTelemetry(): boolean { return !!this.args['disable-telemetry']; }
|
||||
|
||||
@@ -35,9 +35,7 @@ export interface IEnvironmentMainService extends INativeEnvironmentService {
|
||||
|
||||
// --- config
|
||||
sandbox: boolean;
|
||||
driverVerbose: boolean;
|
||||
disableUpdates: boolean;
|
||||
disableKeytar: boolean;
|
||||
}
|
||||
|
||||
export class EnvironmentMainService extends NativeEnvironmentService implements IEnvironmentMainService {
|
||||
@@ -60,9 +58,6 @@ export class EnvironmentMainService extends NativeEnvironmentService implements
|
||||
@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']; }
|
||||
|
||||
|
||||
@@ -20,9 +20,10 @@ const helpCategories = {
|
||||
export interface Option<OptionType> {
|
||||
type: OptionType;
|
||||
alias?: string;
|
||||
deprecates?: string; // old deprecated id
|
||||
deprecates?: string[]; // old deprecated ids
|
||||
args?: string | string[];
|
||||
description?: string;
|
||||
deprecationMessage?: string;
|
||||
cat?: keyof typeof helpCategories;
|
||||
}
|
||||
|
||||
@@ -49,15 +50,16 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
|
||||
'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-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-extensions.") },
|
||||
'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extensions."), args: 'category' },
|
||||
'install-extension': { type: 'string[]', cat: 'e', args: 'extension-id[@version] | path-to-vsix', description: localize('installExtension', "Installs or updates the extension. The identifier of an extension is always `${publisher}.${name}`. Use `--force` argument to update to latest version. 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.") },
|
||||
'install-extension': { type: 'string[]', cat: 'e', args: 'ext-id | path', description: localize('installExtension', "Installs or updates an extension. The argument is either an extension id or a path to a VSIX. The identifier of an extension is '${publisher}.${name}'. Use '--force' argument to update to latest version. To install a specific version provide '@${version}'. For example: 'vscode.csharp@1.2.3'.") },
|
||||
'pre-release': { type: 'boolean', cat: 'e', description: localize('install prerelease', "Installs the pre-release version of the extension, when using --install-extension") },
|
||||
'uninstall-extension': { type: 'string[]', cat: 'e', args: 'ext-id', description: localize('uninstallExtension', "Uninstalls an extension.") },
|
||||
'enable-proposed-api': { type: 'string[]', cat: 'e', args: 'ext-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).") },
|
||||
@@ -68,13 +70,14 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
|
||||
'no-cached-data': { type: 'boolean' },
|
||||
'prof-startup-prefix': { type: 'string' },
|
||||
'prof-v8-extensions': { type: 'boolean' },
|
||||
'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'] },
|
||||
'disable-extensions': { type: 'boolean', deprecates: ['disableExtensions'], cat: 't', description: localize('disableExtensions', "Disable all installed extensions.") },
|
||||
'disable-extension': { type: 'string[]', cat: 't', args: 'ext-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.") },
|
||||
'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.") },
|
||||
'ms-enable-electron-run-as-node': { type: 'boolean' },
|
||||
'max-memory': { type: 'string', cat: 't', description: localize('maxMemory', "Max memory size for a window (in Mbytes)."), args: 'memory' },
|
||||
'telemetry': { type: 'boolean', cat: 't', description: localize('telemetry', "Shows all telemetry events which VS code collects.") },
|
||||
|
||||
@@ -91,11 +94,11 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
|
||||
'debugRenderer': { type: 'boolean' },
|
||||
'inspect-ptyhost': { type: 'string' },
|
||||
'inspect-brk-ptyhost': { type: 'string' },
|
||||
'inspect-search': { type: 'string', deprecates: 'debugSearch' },
|
||||
'inspect-brk-search': { type: 'string', deprecates: 'debugBrkSearch' },
|
||||
'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' },
|
||||
'enable-smoke-test-driver': { type: 'boolean' },
|
||||
'logExtensionHostCommunication': { type: 'boolean' },
|
||||
'skip-release-notes': { type: 'boolean' },
|
||||
'skip-welcome': { type: 'boolean' },
|
||||
@@ -111,7 +114,6 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
|
||||
'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' },
|
||||
@@ -155,6 +157,7 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
|
||||
'log-net-log': { type: 'string' },
|
||||
'vmodule': { type: 'string' },
|
||||
'_urls': { type: 'string[]' },
|
||||
'disable-dev-shm-usage': { type: 'boolean' },
|
||||
|
||||
_: { type: 'string[]' } // main arguments
|
||||
};
|
||||
@@ -162,16 +165,18 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
|
||||
export interface ErrorReporter {
|
||||
onUnknownOption(id: string): void;
|
||||
onMultipleValues(id: string, usedValue: string): void;
|
||||
onDeprecatedOption(deprecatedId: string, message: string): void;
|
||||
}
|
||||
|
||||
const ignoringReporter: ErrorReporter = {
|
||||
onUnknownOption: () => { },
|
||||
onMultipleValues: () => { }
|
||||
onMultipleValues: () => { },
|
||||
onDeprecatedOption: () => { }
|
||||
};
|
||||
|
||||
export function parseArgs<T>(args: string[], options: OptionDescriptions<T>, errorReporter: ErrorReporter = ignoringReporter): T {
|
||||
const alias: { [key: string]: string } = {};
|
||||
const string: string[] = [];
|
||||
const string: string[] = ['_'];
|
||||
const boolean: string[] = [];
|
||||
for (let optionId in options) {
|
||||
const o = options[optionId];
|
||||
@@ -182,12 +187,12 @@ export function parseArgs<T>(args: string[], options: OptionDescriptions<T>, err
|
||||
if (o.type === 'string' || o.type === 'string[]') {
|
||||
string.push(optionId);
|
||||
if (o.deprecates) {
|
||||
string.push(o.deprecates);
|
||||
string.push(...o.deprecates);
|
||||
}
|
||||
} else if (o.type === 'boolean') {
|
||||
boolean.push(optionId);
|
||||
if (o.deprecates) {
|
||||
boolean.push(o.deprecates);
|
||||
boolean.push(...o.deprecates);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,11 +214,18 @@ export function parseArgs<T>(args: string[], options: OptionDescriptions<T>, err
|
||||
}
|
||||
|
||||
let val = remainingArgs[optionId];
|
||||
if (o.deprecates && remainingArgs.hasOwnProperty(o.deprecates)) {
|
||||
if (!val) {
|
||||
val = remainingArgs[o.deprecates];
|
||||
if (o.deprecates) {
|
||||
for (const deprecatedId of o.deprecates) {
|
||||
if (remainingArgs.hasOwnProperty(deprecatedId)) {
|
||||
if (!val) {
|
||||
val = remainingArgs[deprecatedId];
|
||||
if (val) {
|
||||
errorReporter.onDeprecatedOption(deprecatedId, o.deprecationMessage || localize('deprecated.useInstead', 'Use {0} instead.', optionId));
|
||||
}
|
||||
}
|
||||
delete remainingArgs[deprecatedId];
|
||||
}
|
||||
}
|
||||
delete remainingArgs[o.deprecates];
|
||||
}
|
||||
|
||||
if (typeof val !== 'undefined') {
|
||||
@@ -228,6 +240,10 @@ export function parseArgs<T>(args: string[], options: OptionDescriptions<T>, err
|
||||
}
|
||||
}
|
||||
cleanedArgs[optionId] = val;
|
||||
|
||||
if (o.deprecationMessage) {
|
||||
errorReporter.onDeprecatedOption(optionId, o.deprecationMessage);
|
||||
}
|
||||
}
|
||||
delete remainingArgs[optionId];
|
||||
}
|
||||
@@ -298,14 +314,15 @@ function wrapText(text: string, columns: number): string[] {
|
||||
return lines;
|
||||
}
|
||||
|
||||
export function buildHelpMessage(productName: string, executableName: string, version: string, options: OptionDescriptions<any>, isPipeSupported = true): string {
|
||||
export function buildHelpMessage(productName: string, executableName: string, version: string, options: OptionDescriptions<any>, capabilities?: { noPipe?: boolean; noInputFiles: boolean }): string {
|
||||
const columns = (process.stdout).isTTY && (process.stdout).columns || 80;
|
||||
const inputFiles = capabilities?.noInputFiles !== true ? `[${localize('paths', 'paths')}...]` : '';
|
||||
|
||||
let help = [`${productName} ${version}`];
|
||||
const help = [`${productName} ${version}`];
|
||||
help.push('');
|
||||
help.push(`${localize('usage', "Usage")}: ${executableName} [${localize('options', "options")}][${localize('paths', 'paths')}...]`);
|
||||
help.push(`${localize('usage', "Usage")}: ${executableName} [${localize('options', "options")}]${inputFiles}`);
|
||||
help.push('');
|
||||
if (isPipeSupported) {
|
||||
if (capabilities?.noPipe !== true) {
|
||||
if (isWindows) {
|
||||
help.push(localize('stdinWindows', "To read output from another program, append '-' (e.g. 'echo Hello World | {0} -')", executableName));
|
||||
} else {
|
||||
|
||||
@@ -8,7 +8,8 @@ import { IProcessEnvironment } from 'vs/base/common/platform';
|
||||
import { localize } from 'vs/nls';
|
||||
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
|
||||
import { ErrorReporter, OPTIONS, parseArgs } from 'vs/platform/environment/node/argv';
|
||||
import { MIN_MAX_MEMORY_SIZE_MB } from 'vs/platform/files/common/files';
|
||||
|
||||
const MIN_MAX_MEMORY_SIZE_MB = 2048;
|
||||
|
||||
function parseAndValidate(cmdLineArgs: string[], reportWarnings: boolean): NativeParsedArgs {
|
||||
const errorReporter: ErrorReporter = {
|
||||
@@ -16,7 +17,10 @@ function parseAndValidate(cmdLineArgs: string[], reportWarnings: boolean): Nativ
|
||||
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));
|
||||
console.warn(localize('multipleValues', "Option '{0}' is defined more than once. Using value '{1}'.", id, val));
|
||||
},
|
||||
onDeprecatedOption: (deprecatedOption: string, message: string) => {
|
||||
console.warn(localize('deprecatedArgument', "Option '{0}' is deprecated: {1}", deprecatedOption, message));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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 fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as paths from 'vs/base/common/path';
|
||||
|
||||
import { createWriteStream } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { randomPath } from 'vs/base/common/extpath';
|
||||
import { resolveTerminalEncoding } from 'vs/base/node/terminalEncoding';
|
||||
|
||||
export function hasStdinWithoutTty() {
|
||||
@@ -36,17 +34,17 @@ export function stdinDataListener(durationinMs: number): Promise<boolean> {
|
||||
}
|
||||
|
||||
export function getStdinFilePath(): string {
|
||||
return paths.join(os.tmpdir(), `code-stdin-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 3)}`);
|
||||
return randomPath(tmpdir(), 'code-stdin', 3);
|
||||
}
|
||||
|
||||
export async function readFromStdin(targetPath: string, verbose: boolean): Promise<void> {
|
||||
|
||||
// open tmp file for writing
|
||||
const stdinFileStream = fs.createWriteStream(targetPath);
|
||||
const stdinFileStream = createWriteStream(targetPath);
|
||||
|
||||
let encoding = await resolveTerminalEncoding(verbose);
|
||||
|
||||
const iconv = await import('iconv-lite-umd');
|
||||
const iconv = await import('@vscode/iconv-lite-umd');
|
||||
if (!iconv.encodingExists(encoding)) {
|
||||
console.log(`Unsupported terminal encoding: ${encoding}, falling back to UTF-8.`);
|
||||
encoding = 'utf8';
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { writeFileSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { randomPath } from 'vs/base/common/extpath';
|
||||
|
||||
export function createWaitMarkerFile(verbose?: boolean): string | undefined {
|
||||
const randomWaitMarkerPath = join(tmpdir(), Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10));
|
||||
const randomWaitMarkerPath = randomPath(tmpdir());
|
||||
|
||||
try {
|
||||
fs.writeFileSync(randomWaitMarkerPath, ''); // use built-in fs to avoid dragging in more dependencies
|
||||
writeFileSync(randomWaitMarkerPath, ''); // use built-in fs to avoid dragging in more dependencies
|
||||
if (verbose) {
|
||||
console.log(`Marker file for --wait created: ${randomWaitMarkerPath}`);
|
||||
}
|
||||
|
||||
@@ -4,22 +4,30 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { isLinux, isWindows } from 'vs/base/common/platform';
|
||||
import { flakySuite } from 'vs/base/test/common/testUtils';
|
||||
import { Encryption } from 'vs/platform/encryption/node/encryptionMainService';
|
||||
|
||||
function testErrorMessage(module: string): string {
|
||||
return `Unable to load "${module}" dependency. It was probably not compiled for the right operating system architecture or had missing build tools.`;
|
||||
}
|
||||
|
||||
suite('Native Modules (all platforms)', () => {
|
||||
flakySuite('Native Modules (all platforms)', () => {
|
||||
|
||||
test('native-is-elevated', async () => {
|
||||
const isElevated = await import('native-is-elevated');
|
||||
assert.ok(typeof isElevated === 'function', testErrorMessage('native-is-elevated '));
|
||||
|
||||
const result = isElevated();
|
||||
assert.ok(typeof result === 'boolean', testErrorMessage('native-is-elevated'));
|
||||
});
|
||||
|
||||
test('native-keymap', async () => {
|
||||
const keyMap = await import('native-keymap');
|
||||
assert.ok(typeof keyMap.getCurrentKeyboardLayout === 'function', testErrorMessage('native-keymap'));
|
||||
|
||||
const result = keyMap.getCurrentKeyboardLayout();
|
||||
assert.ok(result, testErrorMessage('native-keymap'));
|
||||
});
|
||||
|
||||
test('native-watchdog', async () => {
|
||||
@@ -27,35 +35,81 @@ suite('Native Modules (all platforms)', () => {
|
||||
assert.ok(typeof watchDog.start === 'function', testErrorMessage('native-watchdog'));
|
||||
});
|
||||
|
||||
test('node-pty', async () => {
|
||||
(process.type === 'renderer' ? test.skip /* TODO@electron module is not context aware yet and thus cannot load in Electron renderer used by tests */ : test)('node-pty', async () => {
|
||||
const nodePty = await import('node-pty');
|
||||
assert.ok(typeof nodePty.spawn === 'function', testErrorMessage('node-pty'));
|
||||
});
|
||||
|
||||
test('spdlog', async () => {
|
||||
(process.type === 'renderer' ? test.skip /* TODO@electron module is not context aware yet and thus cannot load in Electron renderer used by tests */ : test)('spdlog', async () => {
|
||||
const spdlog = await import('spdlog');
|
||||
assert.ok(typeof spdlog.createRotatingLogger === 'function', testErrorMessage('spdlog'));
|
||||
assert.ok(typeof spdlog.version === 'number', testErrorMessage('spdlog'));
|
||||
});
|
||||
|
||||
test('nsfw', async () => {
|
||||
const nsfWatcher = await import('vscode-nsfw');
|
||||
assert.ok(typeof nsfWatcher === 'function', testErrorMessage('nsfw'));
|
||||
});
|
||||
|
||||
test('parcel', async () => {
|
||||
test('@parcel/watcher', async () => {
|
||||
const parcelWatcher = await import('@parcel/watcher');
|
||||
assert.ok(typeof parcelWatcher.subscribe === 'function', testErrorMessage('parcel'));
|
||||
assert.ok(typeof parcelWatcher.subscribe === 'function', testErrorMessage('@parcel/watcher'));
|
||||
});
|
||||
|
||||
test('sqlite3', async () => {
|
||||
test('@vscode/sqlite3', async () => {
|
||||
const sqlite3 = await import('@vscode/sqlite3');
|
||||
assert.ok(typeof sqlite3.Database === 'function', testErrorMessage('@vscode/sqlite3'));
|
||||
});
|
||||
|
||||
test('vscode-encrypt', async () => {
|
||||
try {
|
||||
const vscodeEncrypt: Encryption = require.__$__nodeRequire('vscode-encrypt');
|
||||
const encrypted = await vscodeEncrypt.encrypt('salt', 'value');
|
||||
const decrypted = await vscodeEncrypt.decrypt('salt', encrypted);
|
||||
|
||||
assert.ok(typeof encrypted === 'string', testErrorMessage('vscode-encrypt'));
|
||||
assert.ok(typeof decrypted === 'string', testErrorMessage('vscode-encrypt'));
|
||||
} catch (error) {
|
||||
if (error.code !== 'MODULE_NOT_FOUND') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('vsda', async () => {
|
||||
try {
|
||||
const vsda: any = require.__$__nodeRequire('vsda');
|
||||
const signer = new vsda.signer();
|
||||
const signed = await signer.sign('value');
|
||||
assert.ok(typeof signed === 'string', testErrorMessage('vsda'));
|
||||
} catch (error) {
|
||||
if (error.code !== 'MODULE_NOT_FOUND') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
(isLinux ? suite.skip : suite)('Native Modules (Windows, macOS)', () => {
|
||||
|
||||
test('keytar', async () => {
|
||||
const keytar = await import('keytar');
|
||||
const name = `VSCode Test ${Math.floor(Math.random() * 1e9)}`;
|
||||
try {
|
||||
await keytar.setPassword(name, 'foo', 'bar');
|
||||
assert.strictEqual(await keytar.findPassword(name), 'bar');
|
||||
assert.strictEqual((await keytar.findCredentials(name)).length, 1);
|
||||
assert.strictEqual(await keytar.getPassword(name, 'foo'), 'bar');
|
||||
await keytar.deletePassword(name, 'foo');
|
||||
assert.strictEqual(await keytar.getPassword(name, 'foo'), null);
|
||||
} catch (err) {
|
||||
try {
|
||||
await keytar.deletePassword(name, 'foo'); // try to clean up
|
||||
} catch { }
|
||||
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
(!isWindows ? suite.skip : suite)('Native Modules (Windows)', () => {
|
||||
|
||||
test('windows-mutex', async () => {
|
||||
(process.type === 'renderer' ? test.skip /* TODO@electron module is not context aware yet and thus cannot load in Electron renderer used by tests */ : test)('windows-mutex', async () => {
|
||||
const mutex = await import('windows-mutex');
|
||||
assert.ok(mutex && typeof mutex.isActive === 'function', testErrorMessage('windows-mutex'));
|
||||
assert.ok(typeof mutex.isActive === 'function', testErrorMessage('windows-mutex'));
|
||||
@@ -64,20 +118,38 @@ suite('Native Modules (all platforms)', () => {
|
||||
test('windows-foreground-love', async () => {
|
||||
const foregroundLove = await import('windows-foreground-love');
|
||||
assert.ok(typeof foregroundLove.allowSetForegroundWindow === 'function', testErrorMessage('windows-foreground-love'));
|
||||
|
||||
const result = foregroundLove.allowSetForegroundWindow(process.pid);
|
||||
assert.ok(typeof result === 'boolean', testErrorMessage('windows-foreground-love'));
|
||||
});
|
||||
|
||||
test('windows-process-tree', async () => {
|
||||
const processTree = await import('windows-process-tree');
|
||||
assert.ok(typeof processTree.getProcessTree === 'function', testErrorMessage('windows-process-tree'));
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
processTree.getProcessTree(process.pid, tree => {
|
||||
if (tree) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(testErrorMessage('windows-process-tree')));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('vscode-windows-registry', async () => {
|
||||
const windowsRegistry = await import('vscode-windows-registry');
|
||||
assert.ok(typeof windowsRegistry.GetStringRegKey === 'function', testErrorMessage('vscode-windows-registry'));
|
||||
test('@vscode/windows-registry', async () => {
|
||||
const windowsRegistry = await import('@vscode/windows-registry');
|
||||
assert.ok(typeof windowsRegistry.GetStringRegKey === 'function', testErrorMessage('@vscode/windows-registry'));
|
||||
|
||||
const result = windowsRegistry.GetStringRegKey('HKEY_LOCAL_MACHINE', 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion', 'EditionID');
|
||||
assert.ok(typeof result === 'string' || typeof result === 'undefined', testErrorMessage('@vscode/windows-registry'));
|
||||
});
|
||||
|
||||
test('vscode-windows-ca-certs', async () => {
|
||||
// @ts-ignore Windows only
|
||||
// @ts-ignore we do not directly depend on this module anymore
|
||||
// but indirectly from our dependency to `vscode-proxy-agent`
|
||||
// we still want to ensure this module can work properly.
|
||||
const windowsCerts = await import('vscode-windows-ca-certs');
|
||||
const store = new windowsCerts.Crypt32();
|
||||
assert.ok(windowsCerts, testErrorMessage('vscode-windows-ca-certs'));
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { Barrier, CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { canceled, getErrorMessage } from 'vs/base/common/errors';
|
||||
import { CancellationError, getErrorMessage } from 'vs/base/common/errors';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { isWeb } from 'vs/base/common/platform';
|
||||
@@ -14,10 +14,10 @@ import { URI } from 'vs/base/common/uri';
|
||||
import * as nls from 'vs/nls';
|
||||
import {
|
||||
DidUninstallExtensionEvent, ExtensionManagementError, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementParticipant, IExtensionManagementService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, InstallOperation, InstallOptions,
|
||||
InstallVSIXOptions, IReportedExtension, StatisticType, UninstallOptions, TargetPlatform, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode
|
||||
InstallVSIXOptions, IExtensionsControlManifest, StatisticType, UninstallOptions, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode
|
||||
} from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { areSameExtensions, ExtensionIdentifierWithVersion, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, getMaliciousExtensionsSet } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
|
||||
import { areSameExtensions, ExtensionKey, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, getMaliciousExtensionsSet } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
@@ -44,7 +44,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private reportedExtensions: Promise<IReportedExtension[]> | undefined;
|
||||
private extensionsControlManifest: Promise<IExtensionsControlManifest> | undefined;
|
||||
private lastReportTimestamp = 0;
|
||||
private readonly installingExtensions = new Map<string, IInstallExtensionTask>();
|
||||
private readonly uninstallingExtensions = new Map<string, IUninstallExtensionTask>();
|
||||
@@ -84,44 +84,18 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
|
||||
}
|
||||
|
||||
async installFromGallery(extension: IGalleryExtension, options: InstallOptions = {}): Promise<ILocalExtension> {
|
||||
if (!this.galleryService.isEnabled()) {
|
||||
throw new ExtensionManagementError(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"), ExtensionManagementErrorCode.Internal);
|
||||
}
|
||||
|
||||
if (!await this.canInstall(extension)) {
|
||||
const targetPlatform = await this.getTargetPlatform();
|
||||
const error = new ExtensionManagementError(nls.localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", extension.identifier.id, this.productService.nameLong, TargetPlatformToString(targetPlatform)), ExtensionManagementErrorCode.Incompatible);
|
||||
this.logService.error(`Cannot install extension.`, extension.identifier.id, error.message);
|
||||
reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
extension = await this.checkAndGetCompatibleVersion(extension, !options.installGivenVersion);
|
||||
if (!this.galleryService.isEnabled()) {
|
||||
throw new ExtensionManagementError(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"), ExtensionManagementErrorCode.Internal);
|
||||
}
|
||||
const compatible = await this.checkAndGetCompatibleVersion(extension, !options.installGivenVersion, !!options.installPreReleaseVersion);
|
||||
return await this.installExtension(compatible.manifest, compatible.extension, options);
|
||||
} catch (error) {
|
||||
this.logService.error(getErrorMessage(error));
|
||||
reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error);
|
||||
throw error;
|
||||
reportTelemetry(this.telemetryService, 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(extension), error });
|
||||
this.logService.error(`Failed to install extension.`, extension.identifier.id);
|
||||
this.logService.error(error);
|
||||
throw toExtensionManagementError(error);
|
||||
}
|
||||
|
||||
const manifest = await this.galleryService.getManifest(extension, CancellationToken.None);
|
||||
if (manifest === null) {
|
||||
const error = new ExtensionManagementError(`Missing manifest for extension ${extension.identifier.id}`, ExtensionManagementErrorCode.Invalid);
|
||||
this.logService.error(`Failed to install extension:`, extension.identifier.id, error.message);
|
||||
reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
/* {{SQL CARBON EDIT}} Remove this check as we don't want to enforce the manifest versions matching since those are often coming directly from the main branch
|
||||
if (manifest.version !== extension.version) {
|
||||
const error = new ExtensionManagementError(`Cannot install '${extension.identifier.id}' extension because of version mismatch in Marketplace`, ExtensionManagementErrorCode.Invalid);
|
||||
this.logService.error(error.message);
|
||||
reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error);
|
||||
throw error;
|
||||
}
|
||||
*/
|
||||
|
||||
return this.installExtension(manifest, extension, options);
|
||||
}
|
||||
|
||||
async uninstall(extension: ILocalExtension, options: UninstallOptions = {}): Promise<void> {
|
||||
@@ -135,7 +109,8 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
|
||||
throw new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"));
|
||||
}
|
||||
|
||||
const galleryExtension = await this.findGalleryExtension(extension);
|
||||
const targetPlatform = await this.getTargetPlatform();
|
||||
const [galleryExtension] = await this.galleryService.getExtensions([{ ...extension.identifier, preRelease: extension.preRelease }], { targetPlatform, compatible: true }, CancellationToken.None);
|
||||
if (!galleryExtension) {
|
||||
throw new Error(nls.localize('Not a Marketplace extension', "Only Marketplace Extensions can be reinstalled"));
|
||||
}
|
||||
@@ -144,15 +119,15 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
|
||||
await this.installFromGallery(galleryExtension);
|
||||
}
|
||||
|
||||
getExtensionsReport(): Promise<IReportedExtension[]> {
|
||||
getExtensionsControlManifest(): Promise<IExtensionsControlManifest> {
|
||||
const now = new Date().getTime();
|
||||
|
||||
if (!this.reportedExtensions || now - this.lastReportTimestamp > 1000 * 60 * 5) { // 5 minute cache freshness
|
||||
this.reportedExtensions = this.updateReportCache();
|
||||
if (!this.extensionsControlManifest || now - this.lastReportTimestamp > 1000 * 60 * 5) { // 5 minute cache freshness
|
||||
this.extensionsControlManifest = this.updateControlCache();
|
||||
this.lastReportTimestamp = now;
|
||||
}
|
||||
|
||||
return this.reportedExtensions;
|
||||
return this.extensionsControlManifest;
|
||||
}
|
||||
|
||||
registerParticipant(participant: IExtensionManagementParticipant): void {
|
||||
@@ -162,7 +137,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
|
||||
protected async installExtension(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallOptions & InstallVSIXOptions): Promise<ILocalExtension> {
|
||||
// only cache gallery extensions tasks
|
||||
if (!URI.isUri(extension)) {
|
||||
let installExtensionTask = this.installingExtensions.get(new ExtensionIdentifierWithVersion(extension.identifier, extension.version).key());
|
||||
let installExtensionTask = this.installingExtensions.get(ExtensionKey.create(extension).toString());
|
||||
if (installExtensionTask) {
|
||||
this.logService.info('Extensions is already requested to install', extension.identifier.id);
|
||||
return installExtensionTask.waitUntilTaskIsFinished();
|
||||
@@ -170,11 +145,11 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
|
||||
options = { ...options, installOnlyNewlyAddedFromExtensionPack: true /* always true for gallery extensions */ };
|
||||
}
|
||||
|
||||
const allInstallExtensionTasks: { task: IInstallExtensionTask, manifest: IExtensionManifest }[] = [];
|
||||
const allInstallExtensionTasks: { task: IInstallExtensionTask; manifest: IExtensionManifest }[] = [];
|
||||
const installResults: (InstallExtensionResult & { local: ILocalExtension })[] = [];
|
||||
const installExtensionTask = this.createInstallExtensionTask(manifest, extension, options);
|
||||
if (!URI.isUri(extension)) {
|
||||
this.installingExtensions.set(new ExtensionIdentifierWithVersion(installExtensionTask.identifier, manifest.version).key(), installExtensionTask);
|
||||
this.installingExtensions.set(ExtensionKey.create(extension).toString(), installExtensionTask);
|
||||
}
|
||||
this._onInstallExtension.fire({ identifier: installExtensionTask.identifier, source: extension });
|
||||
this.logService.info('Installing extension:', installExtensionTask.identifier.id);
|
||||
@@ -186,14 +161,15 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
|
||||
this.logService.info('Installing the extension without checking dependencies and pack', installExtensionTask.identifier.id);
|
||||
} else {
|
||||
try {
|
||||
const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensionsToInstall(installExtensionTask.identifier, manifest, !!options.installOnlyNewlyAddedFromExtensionPack);
|
||||
const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensionsToInstall(installExtensionTask.identifier, manifest, !!options.installOnlyNewlyAddedFromExtensionPack, !!options.installPreReleaseVersion);
|
||||
for (const { gallery, manifest } of allDepsAndPackExtensionsToInstall) {
|
||||
installExtensionHasDependents = installExtensionHasDependents || !!manifest.extensionDependencies?.some(id => areSameExtensions({ id }, installExtensionTask.identifier));
|
||||
if (this.installingExtensions.has(new ExtensionIdentifierWithVersion(gallery.identifier, gallery.version).key())) {
|
||||
const key = ExtensionKey.create(gallery).toString();
|
||||
if (this.installingExtensions.has(key)) {
|
||||
this.logService.info('Extension is already requested to install', gallery.identifier.id);
|
||||
} else {
|
||||
const task = this.createInstallExtensionTask(manifest, gallery, { ...options, donotIncludePackAndDependencies: true });
|
||||
this.installingExtensions.set(new ExtensionIdentifierWithVersion(task.identifier, manifest.version).key(), task);
|
||||
this.installingExtensions.set(key, task);
|
||||
this._onInstallExtension.fire({ identifier: task.identifier, source: gallery });
|
||||
this.logService.info('Installing extension:', task.identifier.id);
|
||||
allInstallExtensionTasks.push({ task, manifest });
|
||||
@@ -211,7 +187,6 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
|
||||
}
|
||||
} else {
|
||||
this.logService.error('Error while preparing to install dependencies and extension packs of the extension:', installExtensionTask.identifier.id);
|
||||
this.logService.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -220,7 +195,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
|
||||
const extensionsToInstallMap = allInstallExtensionTasks.reduce((result, { task, manifest }) => {
|
||||
result.set(task.identifier.id.toLowerCase(), { task, manifest });
|
||||
return result;
|
||||
}, new Map<string, { task: IInstallExtensionTask, manifest: IExtensionManifest }>());
|
||||
}, new Map<string, { task: IInstallExtensionTask; manifest: IExtensionManifest }>());
|
||||
|
||||
while (extensionsToInstallMap.size) {
|
||||
let extensionsToInstall;
|
||||
@@ -241,9 +216,14 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
|
||||
const local = await task.run();
|
||||
await this.joinAllSettled(this.participants.map(participant => participant.postInstall(local, task.source, options, CancellationToken.None)));
|
||||
if (!URI.isUri(task.source)) {
|
||||
reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', getGalleryExtensionTelemetryData(task.source), new Date().getTime() - startTime, undefined);
|
||||
const isUpdate = task.operation === InstallOperation.Update;
|
||||
reportTelemetry(this.telemetryService, isUpdate ? 'extensionGallery:update' : 'extensionGallery:install', {
|
||||
extensionData: getGalleryExtensionTelemetryData(task.source),
|
||||
duration: new Date().getTime() - startTime,
|
||||
durationSinceUpdate: isUpdate ? undefined : new Date().getTime() - task.source.lastUpdated
|
||||
});
|
||||
// In web, report extension install statistics explicitly. In Desktop, statistics are automatically updated while downloading the VSIX.
|
||||
if (isWeb && task.operation === InstallOperation.Install) {
|
||||
if (isWeb && task.operation !== InstallOperation.Update) {
|
||||
try {
|
||||
await this.galleryService.reportStatistic(local.manifest.publisher, local.manifest.name, local.manifest.version, StatisticType.Install);
|
||||
} catch (error) { /* ignore */ }
|
||||
@@ -252,10 +232,9 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
|
||||
installResults.push({ local, identifier: task.identifier, operation: task.operation, source: task.source });
|
||||
} catch (error) {
|
||||
if (!URI.isUri(task.source)) {
|
||||
reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', getGalleryExtensionTelemetryData(task.source), new Date().getTime() - startTime, error);
|
||||
reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(task.source), duration: new Date().getTime() - startTime, error });
|
||||
}
|
||||
this.logService.error('Error while installing the extension:', task.identifier.id);
|
||||
this.logService.error(error);
|
||||
throw error;
|
||||
} finally { extensionsToInstallMap.delete(task.identifier.id.toLowerCase()); }
|
||||
}));
|
||||
@@ -289,18 +268,13 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
|
||||
}
|
||||
}
|
||||
|
||||
this.logService.error(`Failed to install extension:`, installExtensionTask.identifier.id, getErrorMessage(error));
|
||||
this._onDidInstallExtensions.fire(allInstallExtensionTasks.map(({ task }) => ({ identifier: task.identifier, operation: InstallOperation.Install, source: task.source })));
|
||||
|
||||
if (error instanceof Error) {
|
||||
error.name = error && (<ExtensionManagementError>error).code ? (<ExtensionManagementError>error).code : ExtensionManagementErrorCode.Internal;
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
/* Remove the gallery tasks from the cache */
|
||||
for (const { task, manifest } of allInstallExtensionTasks) {
|
||||
for (const { task } of allInstallExtensionTasks) {
|
||||
if (!URI.isUri(task.source)) {
|
||||
const key = new ExtensionIdentifierWithVersion(task.identifier, manifest.version).key();
|
||||
const key = ExtensionKey.create(task.source).toString();
|
||||
if (!this.installingExtensions.delete(key)) {
|
||||
this.logService.warn('Installation task is not found in the cache', key);
|
||||
}
|
||||
@@ -325,7 +299,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
|
||||
return results;
|
||||
}
|
||||
|
||||
private async getAllDepsAndPackExtensionsToInstall(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean): Promise<{ gallery: IGalleryExtension, manifest: IExtensionManifest }[]> {
|
||||
private async getAllDepsAndPackExtensionsToInstall(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean, installPreRelease: boolean): Promise<{ gallery: IGalleryExtension; manifest: IExtensionManifest }[]> {
|
||||
if (!this.galleryService.isEnabled()) {
|
||||
return [];
|
||||
}
|
||||
@@ -333,7 +307,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
|
||||
let installed = await this.getInstalled();
|
||||
const knownIdentifiers = [extensionIdentifier, ...(installed).map(i => i.identifier)];
|
||||
|
||||
const allDependenciesAndPacks: { gallery: IGalleryExtension, manifest: IExtensionManifest }[] = [];
|
||||
const allDependenciesAndPacks: { gallery: IGalleryExtension; manifest: IExtensionManifest }[] = [];
|
||||
const collectDependenciesAndPackExtensionsToInstall = async (extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest): Promise<void> => {
|
||||
const dependecies: string[] = manifest.extensionDependencies || [];
|
||||
const dependenciesAndPackExtensions = [...dependecies];
|
||||
@@ -352,25 +326,27 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
|
||||
if (dependenciesAndPackExtensions.length) {
|
||||
// filter out installed and known extensions
|
||||
const identifiers = [...knownIdentifiers, ...allDependenciesAndPacks.map(r => r.gallery.identifier)];
|
||||
const names = dependenciesAndPackExtensions.filter(id => identifiers.every(galleryIdentifier => !areSameExtensions(galleryIdentifier, { id })));
|
||||
if (names.length) {
|
||||
const galleryResult = await this.galleryService.query({ names, pageSize: dependenciesAndPackExtensions.length }, CancellationToken.None);
|
||||
for (const galleryExtension of galleryResult.firstPage) {
|
||||
const ids = dependenciesAndPackExtensions.filter(id => identifiers.every(galleryIdentifier => !areSameExtensions(galleryIdentifier, { id })));
|
||||
if (ids.length) {
|
||||
const galleryExtensions = await this.galleryService.getExtensions(ids.map(id => ({ id, preRelease: installPreRelease })), CancellationToken.None);
|
||||
for (const galleryExtension of galleryExtensions) {
|
||||
if (identifiers.find(identifier => areSameExtensions(identifier, galleryExtension.identifier))) {
|
||||
continue;
|
||||
}
|
||||
const isDependency = dependecies.some(id => areSameExtensions({ id }, galleryExtension.identifier));
|
||||
if (!isDependency && !await this.canInstall(galleryExtension)) {
|
||||
this.logService.info('Skipping the packed extension as it cannot be installed', galleryExtension.identifier.id);
|
||||
continue;
|
||||
let compatible;
|
||||
try {
|
||||
compatible = await this.checkAndGetCompatibleVersion(galleryExtension, true, installPreRelease);
|
||||
} catch (error) {
|
||||
if (error instanceof ExtensionManagementError && error.code === ExtensionManagementErrorCode.IncompatibleTargetPlatform && !isDependency) {
|
||||
this.logService.info('Skipping the packed extension as it cannot be installed', galleryExtension.identifier.id);
|
||||
continue;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const compatibleExtension = await this.checkAndGetCompatibleVersion(galleryExtension, true);
|
||||
const manifest = await this.galleryService.getManifest(compatibleExtension, CancellationToken.None);
|
||||
if (manifest === null) {
|
||||
throw new ExtensionManagementError(`Missing manifest for extension ${compatibleExtension.identifier.id}`, ExtensionManagementErrorCode.Invalid);
|
||||
}
|
||||
allDependenciesAndPacks.push({ gallery: compatibleExtension, manifest });
|
||||
await collectDependenciesAndPackExtensionsToInstall(compatibleExtension.identifier, manifest);
|
||||
allDependenciesAndPacks.push({ gallery: compatible.extension, manifest: compatible.manifest });
|
||||
await collectDependenciesAndPackExtensionsToInstall(compatible.extension.identifier, compatible.manifest);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,38 +357,68 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
|
||||
return allDependenciesAndPacks.filter(e => !installed.some(i => areSameExtensions(i.identifier, e.gallery.identifier)));
|
||||
}
|
||||
|
||||
private async checkAndGetCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean): Promise<IGalleryExtension> {
|
||||
if (await this.isMalicious(extension)) {
|
||||
private async checkAndGetCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean, installPreRelease: boolean): Promise<{ extension: IGalleryExtension; manifest: IExtensionManifest }> {
|
||||
const report = await this.getExtensionsControlManifest();
|
||||
if (getMaliciousExtensionsSet(report).has(extension.identifier.id)) {
|
||||
throw new ExtensionManagementError(nls.localize('malicious extension', "Can't install '{0}' extension since it was reported to be problematic.", extension.identifier.id), ExtensionManagementErrorCode.Malicious);
|
||||
}
|
||||
|
||||
const compatibleExtension = await this.getCompatibleVersion(extension, fetchCompatibleVersion);
|
||||
if (!compatibleExtension) {
|
||||
if (!!report.unsupportedPreReleaseExtensions && !!report.unsupportedPreReleaseExtensions[extension.identifier.id]) {
|
||||
throw new ExtensionManagementError(nls.localize('unsupported prerelease extension', "Can't install '{0}' extension because it is no longer supported. It is now part of the '{1}' extension as a pre-release version.", extension.identifier.id, report.unsupportedPreReleaseExtensions[extension.identifier.id].displayName), ExtensionManagementErrorCode.UnsupportedPreRelease);
|
||||
}
|
||||
|
||||
if (!await this.canInstall(extension)) {
|
||||
const targetPlatform = await this.getTargetPlatform();
|
||||
throw new ExtensionManagementError(nls.localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", extension.identifier.id, this.productService.nameLong, TargetPlatformToString(targetPlatform)), ExtensionManagementErrorCode.IncompatibleTargetPlatform);
|
||||
}
|
||||
|
||||
const compatibleExtension = await this.getCompatibleVersion(extension, fetchCompatibleVersion, installPreRelease);
|
||||
if (compatibleExtension) {
|
||||
if (installPreRelease && fetchCompatibleVersion && extension.hasPreReleaseVersion && !compatibleExtension.properties.isPreReleaseVersion) {
|
||||
throw new ExtensionManagementError(nls.localize('notFoundCompatiblePrereleaseDependency', "Can't install pre-release version of '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version), ExtensionManagementErrorCode.IncompatiblePreRelease);
|
||||
}
|
||||
} else {
|
||||
/** If no compatible release version is found, check if the extension has a release version or not and throw relevant error */
|
||||
if (!installPreRelease && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0]) {
|
||||
throw new ExtensionManagementError(nls.localize('notFoundReleaseExtension', "Can't install release version of '{0}' extension because it has no release version.", extension.identifier.id), ExtensionManagementErrorCode.ReleaseVersionNotFound);
|
||||
}
|
||||
throw new ExtensionManagementError(nls.localize('notFoundCompatibleDependency', "Can't install '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version), ExtensionManagementErrorCode.Incompatible);
|
||||
}
|
||||
|
||||
return compatibleExtension;
|
||||
this.logService.info('Getting Manifest...', compatibleExtension.identifier.id);
|
||||
const manifest = await this.galleryService.getManifest(compatibleExtension, CancellationToken.None);
|
||||
if (manifest === null) {
|
||||
throw new ExtensionManagementError(`Missing manifest for extension ${extension.identifier.id}`, ExtensionManagementErrorCode.Invalid);
|
||||
}
|
||||
|
||||
/* {{SQL CARBON EDIT}} Remove this check as we don't want to enforce the manifest versions matching since those are often coming directly from the main branch
|
||||
if (manifest.version !== compatibleExtension.version) {
|
||||
throw new ExtensionManagementError(`Cannot install '${extension.identifier.id}' extension because of version mismatch in Marketplace`, ExtensionManagementErrorCode.Invalid);
|
||||
}
|
||||
*/
|
||||
|
||||
return { extension: compatibleExtension, manifest };
|
||||
}
|
||||
|
||||
protected async getCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean): Promise<IGalleryExtension | null> {
|
||||
protected async getCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean, includePreRelease: boolean): Promise<IGalleryExtension | null> {
|
||||
const targetPlatform = await this.getTargetPlatform();
|
||||
let compatibleExtension: IGalleryExtension | null = null;
|
||||
if (await this.galleryService.isExtensionCompatible(extension, targetPlatform)) {
|
||||
|
||||
if (fetchCompatibleVersion && extension.hasPreReleaseVersion && extension.properties.isPreReleaseVersion !== includePreRelease) {
|
||||
compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, preRelease: includePreRelease }], { targetPlatform, compatible: true }, CancellationToken.None))[0] || null;
|
||||
}
|
||||
|
||||
if (!compatibleExtension && await this.galleryService.isExtensionCompatible(extension, includePreRelease, targetPlatform)) {
|
||||
compatibleExtension = extension;
|
||||
}
|
||||
|
||||
if (!compatibleExtension && fetchCompatibleVersion) {
|
||||
compatibleExtension = await this.galleryService.getCompatibleExtension(extension, targetPlatform);
|
||||
compatibleExtension = await this.galleryService.getCompatibleExtension(extension, includePreRelease, targetPlatform);
|
||||
}
|
||||
|
||||
return compatibleExtension;
|
||||
}
|
||||
|
||||
private async isMalicious(extension: IGalleryExtension): Promise<boolean> {
|
||||
const report = await this.getExtensionsReport();
|
||||
return getMaliciousExtensionsSet(report).has(extension.identifier.id);
|
||||
}
|
||||
|
||||
private async unininstallExtension(extension: ILocalExtension, options: UninstallOptions): Promise<void> {
|
||||
const uninstallExtensionTask = this.uninstallingExtensions.get(extension.identifier.id.toLowerCase());
|
||||
if (uninstallExtensionTask) {
|
||||
@@ -434,7 +440,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
|
||||
} else {
|
||||
this.logService.info('Successfully uninstalled extension:', extension.identifier.id);
|
||||
}
|
||||
reportTelemetry(this.telemetryService, 'extensionGallery:uninstall', getLocalExtensionTelemetryData(extension), undefined, error);
|
||||
reportTelemetry(this.telemetryService, 'extensionGallery:uninstall', { extensionData: getLocalExtensionTelemetryData(extension), error });
|
||||
this._onDidUninstallExtension.fire({ identifier: extension.identifier, error: error?.code });
|
||||
};
|
||||
|
||||
@@ -567,33 +573,15 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
|
||||
return installed.filter(e => e.manifest.extensionDependencies && e.manifest.extensionDependencies.some(id => areSameExtensions({ id }, extension.identifier)));
|
||||
}
|
||||
|
||||
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 async updateReportCache(): Promise<IReportedExtension[]> {
|
||||
private async updateControlCache(): Promise<IExtensionsControlManifest> {
|
||||
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;
|
||||
const manifest = await this.galleryService.getExtensionsControlManifest();
|
||||
this.logService.trace(`ExtensionManagementService.refreshControlCache`, manifest);
|
||||
return manifest;
|
||||
} catch (err) {
|
||||
this.logService.trace('ExtensionManagementService.refreshReportedCache - failed to get extension report');
|
||||
return [];
|
||||
this.logService.trace('ExtensionManagementService.refreshControlCache - failed to get extension control manifest');
|
||||
return { malicious: [] };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,12 +609,22 @@ export function joinErrors(errorOrErrors: (Error | string) | (Array<Error | stri
|
||||
}, new Error(''));
|
||||
}
|
||||
|
||||
export function reportTelemetry(telemetryService: ITelemetryService, eventName: string, extensionData: any, duration?: number, error?: Error): void {
|
||||
function toExtensionManagementError(error: Error): ExtensionManagementError {
|
||||
if (error instanceof ExtensionManagementError) {
|
||||
return error;
|
||||
}
|
||||
const e = new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Internal);
|
||||
e.stack = error.stack;
|
||||
return e;
|
||||
}
|
||||
|
||||
export function reportTelemetry(telemetryService: ITelemetryService, eventName: string, { extensionData, duration, error, durationSinceUpdate }: { extensionData: any; duration?: number; durationSinceUpdate?: number; error?: Error }): void {
|
||||
const errorcode = error ? error instanceof ExtensionManagementError ? error.code : ExtensionManagementErrorCode.Internal : undefined;
|
||||
/* __GDPR__
|
||||
"extensionGallery:install" : {
|
||||
"success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"durationSinceUpdate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
|
||||
"errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
|
||||
"recommendationReason": { "retiredFromVersion": "1.23.0", "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
|
||||
"${include}": [
|
||||
@@ -654,7 +652,7 @@ export function reportTelemetry(telemetryService: ITelemetryService, eventName:
|
||||
]
|
||||
}
|
||||
*/
|
||||
telemetryService.publicLogError(eventName, { ...extensionData, success: !error, duration, errorcode });
|
||||
telemetryService.publicLog(eventName, { ...extensionData, success: !error, duration, errorcode, durationSinceUpdate });
|
||||
}
|
||||
|
||||
export abstract class AbstractExtensionTask<T> {
|
||||
@@ -681,7 +679,7 @@ export abstract class AbstractExtensionTask<T> {
|
||||
return new Promise((c, e) => {
|
||||
const disposable = token.onCancellationRequested(() => {
|
||||
disposable.dispose();
|
||||
e(canceled());
|
||||
e(new CancellationError());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { DISABLED_EXTENSIONS_STORAGE_PATH, IExtensionIdentifier, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { DISABLED_EXTENSIONS_STORAGE_PATH, IExtensionIdentifier, IExtensionManagementService, IGlobalExtensionEnablementService, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
|
||||
@@ -14,16 +14,22 @@ export class GlobalExtensionEnablementService extends Disposable implements IGlo
|
||||
|
||||
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 _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,
|
||||
@IExtensionManagementService extensionManagementService: IExtensionManagementService,
|
||||
) {
|
||||
super();
|
||||
this.storageManger = this._register(new StorageManager(storageService));
|
||||
this._register(this.storageManger.onDidChange(extensions => this._onDidChangeEnablement.fire({ extensions, source: 'storage' })));
|
||||
this._register(extensionManagementService.onDidInstallExtensions(e => e.forEach(({ local, operation }) => {
|
||||
if (local && operation === InstallOperation.Migrate) {
|
||||
this._removeFromDisabledExtensions(local.identifier); /* Reset migrated extensions */
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
async enableExtension(extension: IExtensionIdentifier, source?: string): Promise<boolean> {
|
||||
|
||||
@@ -5,27 +5,28 @@
|
||||
|
||||
import { distinct } from 'vs/base/common/arrays';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { canceled, getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { CancellationError, getErrorMessage, isCancellationError } from 'vs/base/common/errors';
|
||||
import { getOrDefault } from 'vs/base/common/objects';
|
||||
import { IPager } from 'vs/base/common/paging';
|
||||
import { isWeb, platform } from 'vs/base/common/platform';
|
||||
import { arch } from 'vs/base/common/process';
|
||||
import { isBoolean } from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IHeaders, IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request';
|
||||
import { IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { DefaultIconPath, getFallbackTargetPlarforms, getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionIdentifierWithVersion, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IReportedExtension, isIExtensionIdentifier, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, TargetPlatform, toTargetPlatform, WEB_EXTENSION_TAG } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { getFallbackTargetPlarforms, getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, getGalleryExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { ExtensionsPolicy, ExtensionsPolicyKey, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; // {{SQL CARBON EDIT}} Add ExtensionsPolicy and ExtensionsPolicyKey
|
||||
import { ExtensionsPolicy, ExtensionsPolicyKey, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions';
|
||||
import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { asJson, asText, IRequestService } from 'vs/platform/request/common/request'; // {{SQL CARBON EDIT}} Remove unused
|
||||
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
|
||||
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
import { ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { getTelemetryLevel, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { asJson, asTextOrError, IRequestService } from 'vs/platform/request/common/request'; // {{SQL CARBON EDIT}} - remove unused
|
||||
import { resolveMarketplaceHeaders } from 'vs/platform/externalServices/common/marketplace';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
const CURRENT_TARGET_PLATFORM = isWeb ? TargetPlatform.WEB : getTargetPlatform(platform, arch);
|
||||
|
||||
@@ -87,23 +88,88 @@ interface IRawGalleryQueryResult {
|
||||
readonly name: string;
|
||||
readonly count: number;
|
||||
}[];
|
||||
}[]
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
|
||||
enum Flags {
|
||||
|
||||
/**
|
||||
* None is used to retrieve only the basic extension details.
|
||||
*/
|
||||
None = 0x0,
|
||||
|
||||
/**
|
||||
* IncludeVersions will return version information for extensions returned
|
||||
*/
|
||||
IncludeVersions = 0x1,
|
||||
|
||||
/**
|
||||
* IncludeFiles will return information about which files were found
|
||||
* within the extension that were stored independent of the manifest.
|
||||
* When asking for files, versions will be included as well since files
|
||||
* are returned as a property of the versions.
|
||||
* These files can be retrieved using the path to the file without
|
||||
* requiring the entire manifest be downloaded.
|
||||
*/
|
||||
IncludeFiles = 0x2,
|
||||
|
||||
/**
|
||||
* Include the Categories and Tags that were added to the extension definition.
|
||||
*/
|
||||
IncludeCategoryAndTags = 0x4,
|
||||
|
||||
/**
|
||||
* Include the details about which accounts the extension has been shared
|
||||
* with if the extension is a private extension.
|
||||
*/
|
||||
IncludeSharedAccounts = 0x8,
|
||||
|
||||
/**
|
||||
* Include properties associated with versions of the extension
|
||||
*/
|
||||
IncludeVersionProperties = 0x10,
|
||||
|
||||
/**
|
||||
* Excluding non-validated extensions will remove any extension versions that
|
||||
* either are in the process of being validated or have failed validation.
|
||||
*/
|
||||
ExcludeNonValidated = 0x20,
|
||||
|
||||
/**
|
||||
* Include the set of installation targets the extension has requested.
|
||||
*/
|
||||
IncludeInstallationTargets = 0x40,
|
||||
|
||||
/**
|
||||
* Include the base uri for assets of this extension
|
||||
*/
|
||||
IncludeAssetUri = 0x80,
|
||||
|
||||
/**
|
||||
* Include the statistics associated with this extension
|
||||
*/
|
||||
IncludeStatistics = 0x100,
|
||||
|
||||
/**
|
||||
* When retrieving versions from a query, only include the latest
|
||||
* version of the extensions that matched. This is useful when the
|
||||
* caller doesn't need all the published versions. It will save a
|
||||
* significant size in the returned payload.
|
||||
*/
|
||||
IncludeLatestVersionOnly = 0x200,
|
||||
Unpublished = 0x1000
|
||||
|
||||
/**
|
||||
* This flag switches the asset uri to use GetAssetByName instead of CDN
|
||||
* When this is used, values of base asset uri and base asset uri fallback are switched
|
||||
* When this is used, source of asset files are pointed to Gallery service always even if CDN is available
|
||||
*/
|
||||
Unpublished = 0x1000,
|
||||
|
||||
/**
|
||||
* Include the details if an extension is in conflict list or not
|
||||
*/
|
||||
IncludeNameConflictInfo = 0x8000,
|
||||
}
|
||||
|
||||
function flagsToString(...flags: Flags[]): string {
|
||||
@@ -137,6 +203,7 @@ const PropertyType = {
|
||||
Dependency: 'Microsoft.VisualStudio.Code.ExtensionDependencies',
|
||||
ExtensionPack: 'Microsoft.VisualStudio.Code.ExtensionPack',
|
||||
Engine: 'Microsoft.VisualStudio.Code.Engine',
|
||||
PreRelease: 'Microsoft.VisualStudio.Code.PreRelease',
|
||||
// {{SQL CARBON EDIT}}
|
||||
AzDataEngine: 'Microsoft.AzDataEngine',
|
||||
LocalizedLanguages: 'Microsoft.VisualStudio.Code.LocalizedLanguages',
|
||||
@@ -158,6 +225,7 @@ interface IQueryState {
|
||||
readonly flags: Flags;
|
||||
readonly criteria: ICriterium[];
|
||||
readonly assetTypes: string[];
|
||||
readonly source?: string;
|
||||
}
|
||||
|
||||
const DefaultQueryState: IQueryState = {
|
||||
@@ -172,23 +240,29 @@ const DefaultQueryState: IQueryState = {
|
||||
|
||||
/* {{SQL CARBON EDIT}} Remove unused
|
||||
type GalleryServiceQueryClassification = {
|
||||
readonly filterTypes: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
readonly sortBy: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
readonly sortOrder: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
readonly duration: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', 'isMeasurement': true };
|
||||
readonly success: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
readonly requestBodySize: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
readonly responseBodySize?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
readonly statusCode?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
readonly errorCode?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
readonly count?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
readonly filterTypes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
|
||||
readonly flags: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
|
||||
readonly sortBy: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
|
||||
readonly sortOrder: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
|
||||
readonly pageNumber: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
|
||||
readonly duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; 'isMeasurement': true };
|
||||
readonly success: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
|
||||
readonly requestBodySize: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
|
||||
readonly responseBodySize?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
|
||||
readonly statusCode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
|
||||
readonly errorCode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
|
||||
readonly count?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
|
||||
readonly source?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
|
||||
};
|
||||
*/
|
||||
|
||||
type QueryTelemetryData = {
|
||||
readonly flags: number;
|
||||
readonly filterTypes: string[];
|
||||
readonly sortBy: string;
|
||||
readonly sortOrder: string;
|
||||
readonly pageNumber: string;
|
||||
readonly source?: string;
|
||||
};
|
||||
|
||||
/* {{SQL CARBON EDIT}} Remove unused
|
||||
@@ -201,7 +275,23 @@ type GalleryServiceQueryEvent = QueryTelemetryData & {
|
||||
readonly errorCode?: string;
|
||||
readonly count?: string;
|
||||
};
|
||||
|
||||
type GalleryServiceAdditionalQueryClassification = {
|
||||
readonly duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; 'isMeasurement': true };
|
||||
readonly count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
|
||||
};
|
||||
|
||||
type GalleryServiceAdditionalQueryEvent = {
|
||||
readonly duration: number;
|
||||
readonly count: number;
|
||||
};
|
||||
*/
|
||||
interface IExtensionCriteria {
|
||||
readonly targetPlatform: TargetPlatform;
|
||||
readonly compatible: boolean;
|
||||
readonly includePreRelease: boolean | (IExtensionIdentifier & { includePreRelease: boolean })[];
|
||||
readonly versions?: (IExtensionIdentifier & { version: string })[];
|
||||
}
|
||||
|
||||
class Query {
|
||||
|
||||
@@ -244,6 +334,10 @@ class Query {
|
||||
return new Query({ ...this.state, assetTypes });
|
||||
}
|
||||
|
||||
withSource(source: string): Query {
|
||||
return new Query({ ...this.state, source });
|
||||
}
|
||||
|
||||
get raw(): any {
|
||||
const { criteria, pageNumber, pageSize, sortBy, sortOrder, flags, assetTypes } = this.state;
|
||||
const filters = [{ criteria, pageNumber, pageSize, sortBy, sortOrder }];
|
||||
@@ -258,8 +352,11 @@ class Query {
|
||||
get telemetryData(): QueryTelemetryData {
|
||||
return {
|
||||
filterTypes: this.state.criteria.map(criterium => String(criterium.filterType)),
|
||||
flags: this.state.flags,
|
||||
sortBy: String(this.sortBy),
|
||||
sortOrder: String(this.sortOrder)
|
||||
sortOrder: String(this.sortOrder),
|
||||
pageNumber: String(this.pageNumber),
|
||||
source: this.state.source
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -306,15 +403,6 @@ function getDownloadAsset(version: IRawGalleryExtensionVersion): IGalleryExtensi
|
||||
};
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
@@ -358,6 +446,11 @@ function getAzureDataStudioEngine(version: IRawGalleryExtensionVersion): string
|
||||
return (values.length > 0 && values[0].value) || '';
|
||||
}
|
||||
|
||||
function isPreReleaseVersion(version: IRawGalleryExtensionVersion): boolean {
|
||||
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.PreRelease) : [];
|
||||
return values.length > 0 && values[0].value === 'true';
|
||||
}
|
||||
|
||||
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) || '';
|
||||
@@ -420,14 +513,18 @@ export function sortExtensionVersions(versions: IRawGalleryExtensionVersion[], p
|
||||
return versions;
|
||||
}
|
||||
|
||||
function toExtensionWithLatestVersion(galleryExtension: IRawGalleryExtension, index: number, query: Query, querySource: string | undefined, targetPlatform: TargetPlatform): IGalleryExtension {
|
||||
const allTargetPlatforms = getAllTargetPlatforms(galleryExtension);
|
||||
let latestVersion = galleryExtension.versions[0];
|
||||
latestVersion = galleryExtension.versions.find(version => version.version === latestVersion.version && isTargetPlatformCompatible(getTargetPlatformForExtensionVersion(version), allTargetPlatforms, targetPlatform)) || latestVersion;
|
||||
return toExtension(galleryExtension, latestVersion, allTargetPlatforms, index, query, querySource);
|
||||
function setTelemetry(extension: IGalleryExtension, index: number, querySource?: string): void {
|
||||
/* __GDPR__FRAGMENT__
|
||||
"GalleryExtensionTelemetryData2" : {
|
||||
"index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
|
||||
"querySource": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
extension.telemetryData = { index, querySource };
|
||||
}
|
||||
|
||||
function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGalleryExtensionVersion, allTargetPlatforms: TargetPlatform[], index: number, query: Query, querySource?: string): IGalleryExtension {
|
||||
function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGalleryExtensionVersion, allTargetPlatforms: TargetPlatform[]): IGalleryExtension {
|
||||
const latestVersion = galleryExtension.versions[0];
|
||||
const assets = <IGalleryExtensionAssets>{
|
||||
manifest: getVersionAsset(version, AssetType.Manifest),
|
||||
readme: getVersionAsset(version, AssetType.Details),
|
||||
@@ -437,7 +534,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
|
||||
download: getDownloadAsset(version),
|
||||
// {{SQL CARBON EDIT}} - Add downloadPage
|
||||
downloadPage: getVersionAsset(version, AssetType.DownloadPage),
|
||||
icon: getIconAsset(version),
|
||||
icon: getVersionAsset(version, AssetType.Icon),
|
||||
coreTranslations: getCoreTranslationAssets(version)
|
||||
};
|
||||
|
||||
@@ -471,24 +568,19 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
|
||||
azDataEngine: getAzureDataStudioEngine(version),
|
||||
localizedLanguages: getLocalizedLanguages(version),
|
||||
targetPlatform: getTargetPlatformForExtensionVersion(version),
|
||||
isPreReleaseVersion: isPreReleaseVersion(version)
|
||||
},
|
||||
hasPreReleaseVersion: isPreReleaseVersion(latestVersion),
|
||||
hasReleaseVersion: true,
|
||||
preview: getIsPreview(galleryExtension.flags),
|
||||
/* __GDPR__FRAGMENT__
|
||||
"GalleryExtensionTelemetryData2" : {
|
||||
"index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
|
||||
"querySource": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
telemetryData: {
|
||||
index: ((query.pageNumber - 1) * query.pageSize) + index,
|
||||
querySource
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
interface IRawExtensionsReport {
|
||||
type PreReleaseMigrationInfo = { id: string; displayName: string; migrateStorage?: boolean; engine?: string };
|
||||
interface IRawExtensionsControlManifest {
|
||||
malicious: string[];
|
||||
slow: string[];
|
||||
unsupported?: IStringDictionary<boolean | { preReleaseExtension: { id: string; displayName: string } }>;
|
||||
migrateToPreRelease?: IStringDictionary<PreReleaseMigrationInfo>;
|
||||
}
|
||||
|
||||
abstract class AbstractExtensionGalleryService implements IExtensionGalleryService {
|
||||
@@ -498,7 +590,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
||||
private extensionsGalleryUrl: string | undefined;
|
||||
private extensionsControlUrl: string | undefined;
|
||||
|
||||
private readonly commonHeadersPromise: Promise<{ [key: string]: string; }>;
|
||||
private readonly commonHeadersPromise: Promise<{ [key: string]: string }>;
|
||||
|
||||
constructor(
|
||||
storageService: IStorageService | undefined,
|
||||
@@ -525,45 +617,54 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
||||
return !!this.extensionsGalleryUrl;
|
||||
}
|
||||
|
||||
async getExtensions(identifiers: ReadonlyArray<IExtensionIdentifier | IExtensionIdentifierWithVersion>, token: CancellationToken): Promise<IGalleryExtension[]> {
|
||||
const result: IGalleryExtension[] = [];
|
||||
let query = new Query()
|
||||
.withFlags(Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties)
|
||||
.withPage(1, identifiers.length)
|
||||
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code')
|
||||
.withFilter(FilterType.ExtensionName, ...identifiers.map(({ id }) => id.toLowerCase()));
|
||||
|
||||
if (identifiers.every(identifier => !(<IExtensionIdentifierWithVersion>identifier).version)) {
|
||||
query = query.withFlags(Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties, Flags.IncludeLatestVersionOnly);
|
||||
}
|
||||
|
||||
const { galleryExtensions } = await this.queryGallery(query, CURRENT_TARGET_PLATFORM, CancellationToken.None);
|
||||
for (let index = 0; index < galleryExtensions.length; index++) {
|
||||
const galleryExtension = galleryExtensions[index];
|
||||
if (!galleryExtension.versions.length) {
|
||||
continue;
|
||||
}
|
||||
const id = getGalleryExtensionId(galleryExtension.publisher.publisherName, galleryExtension.extensionName);
|
||||
const version = (<IExtensionIdentifierWithVersion | undefined>identifiers.find(identifier => areSameExtensions(identifier, { id })))?.version;
|
||||
if (version) {
|
||||
const versionAsset = galleryExtension.versions.find(v => v.version === version);
|
||||
if (versionAsset) {
|
||||
result.push(toExtension(galleryExtension, versionAsset, getAllTargetPlatforms(galleryExtension), index, query));
|
||||
}
|
||||
getExtensions(extensionInfos: ReadonlyArray<IExtensionInfo>, token: CancellationToken): Promise<IGalleryExtension[]>;
|
||||
getExtensions(extensionInfos: ReadonlyArray<IExtensionInfo>, options: IExtensionQueryOptions, token: CancellationToken): Promise<IGalleryExtension[]>;
|
||||
async getExtensions(extensionInfos: ReadonlyArray<IExtensionInfo>, arg1: any, arg2?: any): Promise<IGalleryExtension[]> {
|
||||
const options = CancellationToken.isCancellationToken(arg1) ? {} : arg1 as IExtensionQueryOptions;
|
||||
const token = CancellationToken.isCancellationToken(arg1) ? arg1 : arg2 as CancellationToken;
|
||||
const names: string[] = []; const ids: string[] = [], includePreReleases: (IExtensionIdentifier & { includePreRelease: boolean })[] = [], versions: (IExtensionIdentifier & { version: string })[] = [];
|
||||
let isQueryForReleaseVersionFromPreReleaseVersion = true;
|
||||
for (const extensionInfo of extensionInfos) {
|
||||
if (extensionInfo.uuid) {
|
||||
ids.push(extensionInfo.uuid);
|
||||
} else {
|
||||
result.push(toExtensionWithLatestVersion(galleryExtension, index, query, undefined, CURRENT_TARGET_PLATFORM));
|
||||
names.push(extensionInfo.id);
|
||||
}
|
||||
// Set includePreRelease to true if version is set, because the version can be a pre-release version
|
||||
const includePreRelease = !!(extensionInfo.version || extensionInfo.preRelease);
|
||||
includePreReleases.push({ id: extensionInfo.id, uuid: extensionInfo.uuid, includePreRelease });
|
||||
if (extensionInfo.version) {
|
||||
versions.push({ id: extensionInfo.id, uuid: extensionInfo.uuid, version: extensionInfo.version });
|
||||
}
|
||||
isQueryForReleaseVersionFromPreReleaseVersion = isQueryForReleaseVersionFromPreReleaseVersion && (!!extensionInfo.hasPreRelease && !includePreRelease);
|
||||
}
|
||||
|
||||
return result;
|
||||
if (!ids.length && !names.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let query = new Query().withPage(1, extensionInfos.length);
|
||||
if (ids.length) {
|
||||
query = query.withFilter(FilterType.ExtensionId, ...ids);
|
||||
}
|
||||
if (names.length) {
|
||||
query = query.withFilter(FilterType.ExtensionName, ...names);
|
||||
}
|
||||
if (options.queryAllVersions || isQueryForReleaseVersionFromPreReleaseVersion /* Inlcude all versions if every requested extension is for release version and has pre-release version */) {
|
||||
query = query.withFlags(query.flags, Flags.IncludeVersions);
|
||||
}
|
||||
if (options.source) {
|
||||
query = query.withSource(options.source);
|
||||
}
|
||||
|
||||
const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM, includePreRelease: includePreReleases, versions, compatible: !!options.compatible }, token);
|
||||
if (options.source) {
|
||||
extensions.forEach((e, index) => setTelemetry(e, index, options.source));
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
async getCompatibleExtension(arg1: IExtensionIdentifier | IGalleryExtension, version?: string): Promise<IGalleryExtension | null> {
|
||||
return this.getCompatibleExtensionByEngine(arg1, version);
|
||||
}
|
||||
|
||||
private async getCompatibleExtensionByEngine(arg1: IExtensionIdentifier | IGalleryExtension, version?: string): Promise<IGalleryExtension | null> {
|
||||
const extension: IGalleryExtension | null = isIExtensionIdentifier(arg1) ? null : arg1;
|
||||
async getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null> {
|
||||
// {{SQL CARBON EDIT}}
|
||||
// Change to original version: removed the extension version validation
|
||||
// Reason: This method is used to find the matching gallery extension for the locally installed extension,
|
||||
@@ -572,49 +673,19 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
||||
if (extension) {
|
||||
return Promise.resolve(extension);
|
||||
}
|
||||
const { id, uuid } = <IExtensionIdentifier>arg1; // {{SQL CARBON EDIT}} @anthonydresser remove extension ? extension.identifier
|
||||
let query = new Query()
|
||||
.withFlags(Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeCategoryAndTags, 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, CURRENT_TARGET_PLATFORM, CancellationToken.None);
|
||||
const [rawExtension] = galleryExtensions;
|
||||
if (!rawExtension || !rawExtension.versions.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const allTargetPlatforms = getAllTargetPlatforms(rawExtension);
|
||||
|
||||
if (version) {
|
||||
const versionAsset = rawExtension.versions.filter(v => v.version === version)[0];
|
||||
if (versionAsset) {
|
||||
const extension = toExtension(rawExtension, versionAsset, allTargetPlatforms, 0, query);
|
||||
if (extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.version, this.productService.date)) {
|
||||
return extension;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawVersion = await this.getLastValidExtensionVersion(rawExtension, rawExtension.versions);
|
||||
if (rawVersion) {
|
||||
return toExtension(rawExtension, rawVersion, allTargetPlatforms, 0, query);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async isExtensionCompatible(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise<boolean> {
|
||||
async isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<boolean> {
|
||||
if (!isTargetPlatformCompatible(extension.properties.targetPlatform, extension.allTargetPlatforms, targetPlatform)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!includePreRelease && extension.properties.isPreReleaseVersion) {
|
||||
// Pre-releases are not allowed when include pre-release flag is not set
|
||||
return false;
|
||||
}
|
||||
|
||||
let engine = extension.properties.engine;
|
||||
if (!engine) {
|
||||
const manifest = await this.getManifest(extension, CancellationToken.None);
|
||||
@@ -626,47 +697,35 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
||||
return isEngineValid(engine, this.productService.version, this.productService.date);
|
||||
}
|
||||
|
||||
private async isRawExtensionVersionCompatible(rawExtensionVersion: IRawGalleryExtensionVersion, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform): Promise<boolean> {
|
||||
if (!isTargetPlatformCompatible(getTargetPlatformForExtensionVersion(rawExtensionVersion), allTargetPlatforms, targetPlatform)) {
|
||||
private async isValidVersion(rawGalleryExtensionVersion: IRawGalleryExtensionVersion, versionType: 'release' | 'prerelease' | 'any', compatible: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform): Promise<boolean> {
|
||||
if (!isTargetPlatformCompatible(getTargetPlatformForExtensionVersion(rawGalleryExtensionVersion), allTargetPlatforms, targetPlatform)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const engine = await this.getEngine(rawExtensionVersion);
|
||||
return isEngineValid(engine, this.productService.version, this.productService.date);
|
||||
if (versionType !== 'any' && isPreReleaseVersion(rawGalleryExtensionVersion) !== (versionType === 'prerelease')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (compatible) {
|
||||
const engine = await this.getEngine(rawGalleryExtensionVersion);
|
||||
if (!isEngineValid(engine, this.productService.version, this.productService.date)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
async query(options: IQueryOptions, token: CancellationToken): Promise<IPager<IGalleryExtension>> {
|
||||
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.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties)
|
||||
.withPage(1, pageSize)
|
||||
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code');
|
||||
|
||||
if (options.excludeFlags) {
|
||||
query = query.withFilter(FilterType.ExcludeWithFlags, options.excludeFlags); // {{SQL CARBON EDIT}} exclude extensions matching excludeFlags options
|
||||
}
|
||||
.withPage(1, pageSize);
|
||||
|
||||
if (text) {
|
||||
// Use category filter instead of "category:themes"
|
||||
@@ -711,27 +770,161 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
||||
query = query.withSortOrder(options.sortOrder);
|
||||
}
|
||||
|
||||
const { galleryExtensions, total } = await this.queryGallery(query, CURRENT_TARGET_PLATFORM, token);
|
||||
const extensions = galleryExtensions.map((e, index) => toExtensionWithLatestVersion(e, index, query, options.source, CURRENT_TARGET_PLATFORM));
|
||||
if (options.source) {
|
||||
query = query.withSource(options.source);
|
||||
}
|
||||
|
||||
const runQuery = async (query: Query, token: CancellationToken) => {
|
||||
const { extensions, total } = await this.queryGalleryExtensions(query, { targetPlatform: CURRENT_TARGET_PLATFORM, compatible: false, includePreRelease: !!options.includePreRelease }, token);
|
||||
extensions.forEach((e, index) => setTelemetry(e, ((query.pageNumber - 1) * query.pageSize) + index, options.source));
|
||||
return { extensions, total };
|
||||
};
|
||||
const { extensions, total } = await runQuery(query, token);
|
||||
const getPage = async (pageIndex: number, ct: CancellationToken) => {
|
||||
if (ct.isCancellationRequested) {
|
||||
throw canceled();
|
||||
throw new CancellationError();
|
||||
}
|
||||
const nextPageQuery = query.withPage(pageIndex + 1);
|
||||
const { galleryExtensions } = await this.queryGallery(nextPageQuery, CURRENT_TARGET_PLATFORM, ct);
|
||||
return galleryExtensions.map((e, index) => toExtensionWithLatestVersion(e, index, nextPageQuery, options.source, CURRENT_TARGET_PLATFORM));
|
||||
const { extensions } = await runQuery(query.withPage(pageIndex + 1), ct);
|
||||
return extensions;
|
||||
};
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
return { firstPage: extensions, total, pageSize: extensions.length, getPage } as IPager<IGalleryExtension>;
|
||||
}
|
||||
|
||||
private async queryGalleryExtensions(query: Query, criteria: IExtensionCriteria, token: CancellationToken): Promise<{ extensions: IGalleryExtension[]; total: number }> {
|
||||
const flags = query.flags;
|
||||
|
||||
/**
|
||||
* If both version flags (IncludeLatestVersionOnly and IncludeVersions) are included, then only include latest versions (IncludeLatestVersionOnly) flag.
|
||||
*/
|
||||
if (!!(query.flags & Flags.IncludeLatestVersionOnly) && !!(query.flags & Flags.IncludeVersions)) {
|
||||
query = query.withFlags(query.flags & ~Flags.IncludeVersions, Flags.IncludeLatestVersionOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* If version flags (IncludeLatestVersionOnly and IncludeVersions) are not included, default is to query for latest versions (IncludeLatestVersionOnly).
|
||||
*/
|
||||
if (!(query.flags & Flags.IncludeLatestVersionOnly) && !(query.flags & Flags.IncludeVersions)) {
|
||||
query = query.withFlags(query.flags, Flags.IncludeLatestVersionOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* If versions criteria exist, then remove IncludeLatestVersionOnly flag and add IncludeVersions flag.
|
||||
*/
|
||||
if (criteria.versions?.length) {
|
||||
query = query.withFlags(query.flags & ~Flags.IncludeLatestVersionOnly, Flags.IncludeVersions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add necessary extension flags
|
||||
*/
|
||||
query = query.withFlags(query.flags, Flags.IncludeAssetUri, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeStatistics, Flags.IncludeVersionProperties);
|
||||
const { galleryExtensions: rawGalleryExtensions, total } = await this.queryRawGalleryExtensions(query, token);
|
||||
|
||||
const hasAllVersions: boolean = !(query.flags & Flags.IncludeLatestVersionOnly);
|
||||
if (hasAllVersions) {
|
||||
const extensions: IGalleryExtension[] = [];
|
||||
for (const rawGalleryExtension of rawGalleryExtensions) {
|
||||
const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria);
|
||||
if (extension) {
|
||||
extensions.push(extension);
|
||||
}
|
||||
}
|
||||
return { extensions, total };
|
||||
}
|
||||
|
||||
const result: [number, IGalleryExtension][] = [];
|
||||
const needAllVersions = new Map<string, number>();
|
||||
for (let index = 0; index < rawGalleryExtensions.length; index++) {
|
||||
const rawGalleryExtension = rawGalleryExtensions[index];
|
||||
const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId };
|
||||
const includePreRelease = isBoolean(criteria.includePreRelease) ? criteria.includePreRelease : !!criteria.includePreRelease.find(extensionIdentifierWithPreRelease => areSameExtensions(extensionIdentifierWithPreRelease, extensionIdentifier))?.includePreRelease;
|
||||
if (criteria.compatible && isNotWebExtensionInWebTargetPlatform(getAllTargetPlatforms(rawGalleryExtension), criteria.targetPlatform)) {
|
||||
/** Skip if requested for a web-compatible extension and it is not a web extension.
|
||||
* All versions are not needed in this case
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria);
|
||||
if (!extension
|
||||
/** Need all versions if the extension is a pre-release version but
|
||||
* - the query is to look for a release version or
|
||||
* - the extension has no release version
|
||||
* Get all versions to get or check the release version
|
||||
*/
|
||||
|| (extension.properties.isPreReleaseVersion && (!includePreRelease || !extension.hasReleaseVersion))
|
||||
/**
|
||||
* Need all versions if the extension is a release version with a different target platform than requested and also has a pre-release version
|
||||
* Because, this is a platform specific extension and can have a newer release version supporting this platform.
|
||||
* See https://github.com/microsoft/vscode/issues/139628
|
||||
*/
|
||||
|| (!extension.properties.isPreReleaseVersion && extension.properties.targetPlatform !== criteria.targetPlatform && extension.hasPreReleaseVersion)
|
||||
) {
|
||||
needAllVersions.set(rawGalleryExtension.extensionId, index);
|
||||
} else {
|
||||
result.push([index, extension]);
|
||||
}
|
||||
}
|
||||
|
||||
if (needAllVersions.size) {
|
||||
const query = new Query()
|
||||
.withFlags(flags & ~Flags.IncludeLatestVersionOnly, Flags.IncludeVersions)
|
||||
.withPage(1, needAllVersions.size)
|
||||
.withFilter(FilterType.ExtensionId, ...needAllVersions.keys());
|
||||
const { extensions } = await this.queryGalleryExtensions(query, criteria, token);
|
||||
for (const extension of extensions) {
|
||||
const index = needAllVersions.get(extension.identifier.uuid)!;
|
||||
result.push([index, extension]);
|
||||
}
|
||||
}
|
||||
|
||||
return { extensions: result.sort((a, b) => a[0] - b[0]).map(([, extension]) => extension), total };
|
||||
}
|
||||
|
||||
private async toGalleryExtensionWithCriteria(rawGalleryExtension: IRawGalleryExtension, criteria: IExtensionCriteria): Promise<IGalleryExtension | null> {
|
||||
|
||||
const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId };
|
||||
const version = criteria.versions?.find(extensionIdentifierWithVersion => areSameExtensions(extensionIdentifierWithVersion, extensionIdentifier))?.version;
|
||||
const includePreRelease = isBoolean(criteria.includePreRelease) ? criteria.includePreRelease : !!criteria.includePreRelease.find(extensionIdentifierWithPreRelease => areSameExtensions(extensionIdentifierWithPreRelease, extensionIdentifier))?.includePreRelease;
|
||||
const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension);
|
||||
const rawGalleryExtensionVersions = sortExtensionVersions(rawGalleryExtension.versions, criteria.targetPlatform);
|
||||
|
||||
if (criteria.compatible && isNotWebExtensionInWebTargetPlatform(allTargetPlatforms, criteria.targetPlatform)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let index = 0; index < rawGalleryExtensionVersions.length; index++) {
|
||||
const rawGalleryExtensionVersion = rawGalleryExtensionVersions[index];
|
||||
if (version && rawGalleryExtensionVersion.version !== version) {
|
||||
continue;
|
||||
}
|
||||
// Allow any version if includePreRelease flag is set otherwise only release versions are allowed
|
||||
if (await this.isValidVersion(rawGalleryExtensionVersion, includePreRelease ? 'any' : 'release', criteria.compatible, allTargetPlatforms, criteria.targetPlatform)) {
|
||||
return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms);
|
||||
}
|
||||
if (version && rawGalleryExtensionVersion.version === version) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (version || criteria.compatible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback: Return the latest version
|
||||
* This can happen when the extension does not have a release version or does not have a version compatible with the given target platform.
|
||||
*/
|
||||
return toExtension(rawGalleryExtension, rawGalleryExtension.versions[0], allTargetPlatforms);
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
/**
|
||||
* The result of querying the gallery returns all the extensions because it's only reading a static file.
|
||||
* So this method should apply all the filters and return the actual result
|
||||
*/
|
||||
private createQueryResult(query: Query, galleryExtensions: IRawGalleryExtension[]): { galleryExtensions: IRawGalleryExtension[], total: number; } {
|
||||
private createQueryResult(query: Query, galleryExtensions: IRawGalleryExtension[]): { galleryExtensions: IRawGalleryExtension[]; total: number } {
|
||||
|
||||
// Filtering
|
||||
let filteredExtensions = galleryExtensions;
|
||||
@@ -752,7 +945,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
||||
// we only have 1 version for our extensions in the gallery file, so this should always be the case
|
||||
if (e.versions.length === 1) {
|
||||
const allTargetPlatforms = getAllTargetPlatforms(e);
|
||||
const extension = toExtension(e, e.versions[0], allTargetPlatforms, 0, query);
|
||||
const extension = toExtension(e, e.versions[0], allTargetPlatforms);
|
||||
return extension.properties.localizedLanguages && extension.properties.localizedLanguages.length > 0;
|
||||
}
|
||||
return false;
|
||||
@@ -841,13 +1034,15 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
||||
return a[fieldName] < b[fieldName] ? -1 : 1;
|
||||
}
|
||||
|
||||
private async queryGallery(query: Query, targetPlatform: TargetPlatform, token: CancellationToken): Promise<{ galleryExtensions: IRawGalleryExtension[], total: number; }> {
|
||||
private async queryRawGalleryExtensions(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
|
||||
/* Always exclude non validated extensions */
|
||||
.withFlags(query.flags, Flags.ExcludeNonValidated)
|
||||
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code')
|
||||
/* Always exclude unpublished extensions */
|
||||
.withFilter(FilterType.ExcludeWithFlags, flagsToString(Flags.Unpublished));
|
||||
|
||||
const commonHeaders = await this.commonHeadersPromise;
|
||||
@@ -878,13 +1073,13 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
||||
if (result) {
|
||||
const r = result.results[0];
|
||||
const galleryExtensions = r.extensions;
|
||||
galleryExtensions.forEach(e => sortExtensionVersions(e.versions, targetPlatform));
|
||||
// {{SQL CARBON TODO}}
|
||||
galleryExtensions.forEach(e => sortExtensionVersions(e.versions, TargetPlatform.UNIVERSAL));
|
||||
// const resultCount = r.resultMetadata && r.resultMetadata.filter(m => m.metadataType === 'ResultCount')[0]; {{SQL CARBON EDIT}} comment out for no unused
|
||||
// const total = resultCount && resultCount.metadataItems.filter(i => i.name === 'TotalCount')[0].count || 0; {{SQL CARBON EDIT}} comment out for no unused
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
let filteredExtensionsResult = this.createQueryResult(query, galleryExtensions);
|
||||
|
||||
return { galleryExtensions: filteredExtensionsResult.galleryExtensions, total: filteredExtensionsResult.total };
|
||||
// {{SQL CARBON EDIT}} - End
|
||||
}
|
||||
@@ -940,7 +1135,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
||||
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);
|
||||
const content = await asTextOrError(context);
|
||||
return content || '';
|
||||
}
|
||||
return '';
|
||||
@@ -949,7 +1144,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
||||
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);
|
||||
const text = await asTextOrError(context);
|
||||
return text ? JSON.parse(text) : null;
|
||||
}
|
||||
return null;
|
||||
@@ -969,7 +1164,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
||||
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);
|
||||
const text = await asTextOrError(context);
|
||||
return text ? JSON.parse(text) : null;
|
||||
}
|
||||
return null;
|
||||
@@ -978,17 +1173,16 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
||||
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);
|
||||
const content = await asTextOrError(context);
|
||||
return content || '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async getAllCompatibleVersions(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise<IGalleryExtensionVersion[]> {
|
||||
async getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtensionVersion[]> {
|
||||
let query = new Query()
|
||||
.withFlags(Flags.IncludeVersions, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties)
|
||||
.withPage(1, 1)
|
||||
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code');
|
||||
.withPage(1, 1);
|
||||
|
||||
if (extension.identifier.uuid) {
|
||||
query = query.withFilter(FilterType.ExtensionId, extension.identifier.uuid);
|
||||
@@ -996,7 +1190,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
||||
query = query.withFilter(FilterType.ExtensionName, extension.identifier.id);
|
||||
}
|
||||
|
||||
const { galleryExtensions } = await this.queryGallery(query, targetPlatform, CancellationToken.None);
|
||||
const { galleryExtensions } = await this.queryRawGalleryExtensions(query, CancellationToken.None);
|
||||
if (!galleryExtensions.length) {
|
||||
return [];
|
||||
}
|
||||
@@ -1006,14 +1200,24 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: IGalleryExtensionVersion[] = [];
|
||||
for (const version of galleryExtensions[0].versions) {
|
||||
const validVersions: IRawGalleryExtensionVersion[] = [];
|
||||
await Promise.all(galleryExtensions[0].versions.map(async (version) => {
|
||||
try {
|
||||
if (result[result.length - 1]?.version !== version.version && await this.isRawExtensionVersionCompatible(version, allTargetPlatforms, targetPlatform)) {
|
||||
result.push({ version: version.version, date: version.lastUpdated });
|
||||
if (await this.isValidVersion(version, includePreRelease ? 'any' : 'release', true, allTargetPlatforms, targetPlatform)) {
|
||||
validVersions.push(version);
|
||||
}
|
||||
} catch (error) { /* Ignore error and skip version */ }
|
||||
}));
|
||||
|
||||
const result: IGalleryExtensionVersion[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const version of sortExtensionVersions(validVersions, targetPlatform)) {
|
||||
if (!seen.has(version.version)) {
|
||||
seen.add(version.version);
|
||||
result.push({ version: version.version, date: version.lastUpdated, isPreReleaseVersion: isPreReleaseVersion(version) });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1032,17 +1236,17 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
||||
if (context.res.statusCode === 200) {
|
||||
return context;
|
||||
}
|
||||
const message = await asText(context);
|
||||
const message = await asTextOrError(context);
|
||||
throw new Error(`Expected 200, got back ${context.res.statusCode} instead.\n\n${message}`);
|
||||
} catch (err) {
|
||||
if (isPromiseCanceledError(err)) {
|
||||
if (isCancellationError(err)) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
const message = getErrorMessage(err);
|
||||
type GalleryServiceCDNFallbackClassification = {
|
||||
url: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
message: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
url: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
|
||||
message: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
|
||||
};
|
||||
type GalleryServiceCDNFallbackEvent = {
|
||||
url: string;
|
||||
@@ -1054,31 +1258,31 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
||||
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 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) {
|
||||
// {{SQL CARBON EDIT}}
|
||||
const vsCodeEngine = getEngine(version);
|
||||
const azDataEngine = getAzureDataStudioEngine(version);
|
||||
// Require at least one engine version
|
||||
if (!vsCodeEngine && !azDataEngine) {
|
||||
return null;
|
||||
}
|
||||
const vsCodeEngineValid = !vsCodeEngine || (vsCodeEngine && isEngineValid(vsCodeEngine, this.productService.vscodeVersion, this.productService.date));
|
||||
const azDataEngineValid = !azDataEngine || (azDataEngine && isEngineValid(azDataEngine, this.productService.version, this.productService.date));
|
||||
if (vsCodeEngineValid && azDataEngineValid) {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// private getLastValidExtensionVersionFromProperties(extension: IRawGalleryExtension, versions: IRawGalleryExtensionVersion[]): IRawGalleryExtensionVersion | null {
|
||||
// for (const version of versions) {
|
||||
// // {{SQL CARBON EDIT}}
|
||||
// const vsCodeEngine = getEngine(version);
|
||||
// const azDataEngine = getAzureDataStudioEngine(version);
|
||||
// // Require at least one engine version
|
||||
// if (!vsCodeEngine && !azDataEngine) {
|
||||
// return null;
|
||||
// }
|
||||
// const vsCodeEngineValid = !vsCodeEngine || (vsCodeEngine && isEngineValid(vsCodeEngine, this.productService.vscodeVersion, this.productService.date));
|
||||
// const azDataEngineValid = !azDataEngine || (azDataEngine && isEngineValid(azDataEngine, this.productService.version, this.productService.date));
|
||||
// if (vsCodeEngineValid && azDataEngineValid) {
|
||||
// return version;
|
||||
// }
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
|
||||
private async getEngine(rawExtensionVersion: IRawGalleryExtensionVersion): Promise<string> {
|
||||
let engine = getEngine(rawExtensionVersion);
|
||||
@@ -1092,30 +1296,30 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
||||
return engine;
|
||||
}
|
||||
|
||||
private async getLastValidExtensionVersionRecursively(extension: IRawGalleryExtension, versions: IRawGalleryExtensionVersion[]): Promise<IRawGalleryExtensionVersion | null> {
|
||||
if (!versions.length) {
|
||||
return null;
|
||||
}
|
||||
// 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, this.productService.date)) {
|
||||
return this.getLastValidExtensionVersionRecursively(extension, versions.slice(1));
|
||||
}
|
||||
// const version = versions[0];
|
||||
// const engine = await this.getEngine(version);
|
||||
// if (!isEngineValid(engine, this.productService.version, this.productService.date)) {
|
||||
// return this.getLastValidExtensionVersionRecursively(extension, versions.slice(1));
|
||||
// }
|
||||
|
||||
return {
|
||||
...version,
|
||||
properties: [...(version.properties || []), { key: PropertyType.Engine, value: engine }]
|
||||
};
|
||||
}
|
||||
// return {
|
||||
// ...version,
|
||||
// properties: [...(version.properties || []), { key: PropertyType.Engine, value: engine }]
|
||||
// };
|
||||
// }
|
||||
|
||||
async getExtensionsReport(): Promise<IReportedExtension[]> {
|
||||
async getExtensionsControlManifest(): Promise<IExtensionsControlManifest> {
|
||||
if (!this.isEnabled()) {
|
||||
throw new Error('No extension gallery service configured.');
|
||||
}
|
||||
|
||||
if (!this.extensionsControlUrl) {
|
||||
return [];
|
||||
return { malicious: [] };
|
||||
}
|
||||
|
||||
const context = await this.requestService.request({ type: 'GET', url: this.extensionsControlUrl }, CancellationToken.None);
|
||||
@@ -1123,18 +1327,32 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
||||
throw new Error('Could not get extensions report.');
|
||||
}
|
||||
|
||||
const result = await asJson<IRawExtensionsReport>(context);
|
||||
const map = new Map<string, IReportedExtension>();
|
||||
const result = await asJson<IRawExtensionsControlManifest>(context);
|
||||
const malicious: IExtensionIdentifier[] = [];
|
||||
const unsupportedPreReleaseExtensions: IStringDictionary<{ id: string; displayName: string; migrateStorage?: boolean }> = {};
|
||||
|
||||
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);
|
||||
malicious.push({ id });
|
||||
}
|
||||
if (result.unsupported) {
|
||||
for (const extensionId of Object.keys(result.unsupported)) {
|
||||
const value = result.unsupported[extensionId];
|
||||
if (!isBoolean(value)) {
|
||||
unsupportedPreReleaseExtensions[extensionId.toLowerCase()] = value.preReleaseExtension;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.migrateToPreRelease) {
|
||||
for (const [unsupportedPreReleaseExtensionId, preReleaseExtensionInfo] of Object.entries(result.migrateToPreRelease)) {
|
||||
if (!preReleaseExtensionInfo.engine || isEngineValid(preReleaseExtensionInfo.engine, this.productService.version, this.productService.date)) {
|
||||
unsupportedPreReleaseExtensions[unsupportedPreReleaseExtensionId.toLowerCase()] = preReleaseExtensionInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...map.values()];
|
||||
return { malicious, unsupportedPreReleaseExtensions };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1168,18 +1386,3 @@ export class ExtensionGalleryServiceWithNoStorageService extends AbstractExtensi
|
||||
super(undefined, requestService, logService, environmentService, telemetryService, fileService, productService, configurationService);
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveMarketplaceHeaders(version: string, productService: IProductService, environmentService: IEnvironmentService, configurationService: IConfigurationService, fileService: IFileService, storageService: {
|
||||
get: (key: string, scope: StorageScope) => string | undefined,
|
||||
store: (key: string, value: string, scope: StorageScope, target: StorageTarget) => 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);
|
||||
if (supportsTelemetry(productService, environmentService) && getTelemetryLevel(configurationService) === TelemetryLevel.USAGE) {
|
||||
headers['X-Market-User-Id'] = uuid;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
@@ -4,41 +4,19 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { FileAccess } from 'vs/base/common/network';
|
||||
import { IPager } from 'vs/base/common/paging';
|
||||
import { Platform } from 'vs/base/common/platform';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { localize } from 'vs/nls';
|
||||
import { ExtensionType, IExtension, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
|
||||
import { ExtensionType, IExtension, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
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 const WEB_EXTENSION_TAG = '__web_extension';
|
||||
|
||||
export const enum TargetPlatform {
|
||||
WIN32_X64 = 'win32-x64',
|
||||
WIN32_IA32 = 'win32-ia32',
|
||||
WIN32_ARM64 = 'win32-arm64',
|
||||
|
||||
LINUX_X64 = 'linux-x64',
|
||||
LINUX_ARM64 = 'linux-arm64',
|
||||
LINUX_ARMHF = 'linux-armhf',
|
||||
|
||||
ALPINE_X64 = 'alpine-x64',
|
||||
ALPINE_ARM64 = 'alpine-arm64',
|
||||
|
||||
DARWIN_X64 = 'darwin-x64',
|
||||
DARWIN_ARM64 = 'darwin-arm64',
|
||||
|
||||
WEB = 'web',
|
||||
|
||||
UNIVERSAL = 'universal',
|
||||
UNKNOWN = 'unknown',
|
||||
UNDEFINED = 'undefined',
|
||||
}
|
||||
|
||||
export function TargetPlatformToString(targetPlatform: TargetPlatform) {
|
||||
switch (targetPlatform) {
|
||||
case TargetPlatform.WIN32_X64: return 'Windows 64 bit';
|
||||
@@ -186,6 +164,7 @@ export interface IGalleryExtensionProperties {
|
||||
azDataEngine?: string;
|
||||
localizedLanguages?: string[];
|
||||
targetPlatform: TargetPlatform;
|
||||
isPreReleaseVersion: boolean;
|
||||
}
|
||||
|
||||
export interface IGalleryExtensionAsset {
|
||||
@@ -202,7 +181,7 @@ export interface IGalleryExtensionAssets {
|
||||
download: IGalleryExtensionAsset;
|
||||
// {{SQL CARBON EDIT}}
|
||||
downloadPage?: IGalleryExtensionAsset;
|
||||
icon: IGalleryExtensionAsset;
|
||||
icon: IGalleryExtensionAsset | null;
|
||||
coreTranslations: [string, IGalleryExtensionAsset][];
|
||||
}
|
||||
|
||||
@@ -213,23 +192,11 @@ export function isIExtensionIdentifier(thing: any): thing is IExtensionIdentifie
|
||||
&& (!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;
|
||||
}
|
||||
@@ -237,6 +204,7 @@ export interface IGalleryExtensionIdentifier extends IExtensionIdentifier {
|
||||
export interface IGalleryExtensionVersion {
|
||||
version: string;
|
||||
date: string;
|
||||
isPreReleaseVersion: boolean;
|
||||
}
|
||||
|
||||
export interface IGalleryExtension {
|
||||
@@ -247,7 +215,7 @@ export interface IGalleryExtension {
|
||||
publisherId: string;
|
||||
publisher: string;
|
||||
publisherDisplayName: string;
|
||||
publisherDomain?: { link: string, verified: boolean };
|
||||
publisherDomain?: { link: string; verified: boolean };
|
||||
description: string;
|
||||
installCount: number;
|
||||
rating: number;
|
||||
@@ -257,23 +225,32 @@ export interface IGalleryExtension {
|
||||
releaseDate: number;
|
||||
lastUpdated: number;
|
||||
preview: boolean;
|
||||
hasPreReleaseVersion: boolean;
|
||||
hasReleaseVersion: boolean;
|
||||
allTargetPlatforms: TargetPlatform[];
|
||||
assets: IGalleryExtensionAssets;
|
||||
properties: IGalleryExtensionProperties;
|
||||
telemetryData: any;
|
||||
telemetryData?: any;
|
||||
}
|
||||
|
||||
export interface IGalleryMetadata {
|
||||
id: string;
|
||||
publisherId: string;
|
||||
publisherDisplayName: string;
|
||||
isPreReleaseVersion: boolean;
|
||||
targetPlatform?: TargetPlatform;
|
||||
}
|
||||
|
||||
export type Metadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; isBuiltin: boolean; isSystem: boolean; updated: boolean; preRelease: boolean; installedTimestamp: number }>;
|
||||
|
||||
export interface ILocalExtension extends IExtension {
|
||||
isMachineScoped: boolean;
|
||||
publisherId: string | null;
|
||||
publisherDisplayName: string | null;
|
||||
installedTimestamp?: number;
|
||||
isPreReleaseVersion: boolean;
|
||||
preRelease: boolean;
|
||||
updated: boolean;
|
||||
}
|
||||
|
||||
export const enum SortBy {
|
||||
@@ -301,6 +278,7 @@ export interface IQueryOptions {
|
||||
sortBy?: SortBy;
|
||||
sortOrder?: SortOrder;
|
||||
source?: string;
|
||||
includePreRelease?: boolean;
|
||||
// {{SQL CARBON EDIT}} do not show extensions matching excludeFlags in the marketplace
|
||||
// This field only supports an exact match of a single flag. It doesn't currently
|
||||
// support setting multiple flags such as "hidden,preview" since this functionality isn't
|
||||
@@ -313,39 +291,52 @@ export const enum StatisticType {
|
||||
Uninstall = 'uninstall'
|
||||
}
|
||||
|
||||
export interface IReportedExtension {
|
||||
id: IExtensionIdentifier;
|
||||
malicious: boolean;
|
||||
export interface IExtensionsControlManifest {
|
||||
malicious: IExtensionIdentifier[];
|
||||
unsupportedPreReleaseExtensions?: IStringDictionary<{ id: string; displayName: string; migrateStorage?: boolean }>;
|
||||
}
|
||||
|
||||
export const enum InstallOperation {
|
||||
None = 0,
|
||||
None = 1,
|
||||
Install,
|
||||
Update
|
||||
Update,
|
||||
Migrate,
|
||||
}
|
||||
|
||||
export interface ITranslation {
|
||||
contents: { [key: string]: {} };
|
||||
}
|
||||
|
||||
export interface IExtensionInfo extends IExtensionIdentifier {
|
||||
version?: string;
|
||||
preRelease?: boolean;
|
||||
hasPreRelease?: boolean;
|
||||
}
|
||||
|
||||
export interface IExtensionQueryOptions {
|
||||
targetPlatform?: TargetPlatform;
|
||||
compatible?: boolean;
|
||||
queryAllVersions?: boolean;
|
||||
source?: 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(identifiers: ReadonlyArray<IExtensionIdentifier | IExtensionIdentifierWithVersion>, token: CancellationToken): Promise<IGalleryExtension[]>;
|
||||
getExtensions(extensionInfos: ReadonlyArray<IExtensionInfo>, token: CancellationToken): Promise<IGalleryExtension[]>;
|
||||
getExtensions(extensionInfos: ReadonlyArray<IExtensionInfo>, options: IExtensionQueryOptions, token: CancellationToken): Promise<IGalleryExtension[]>;
|
||||
isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<boolean>;
|
||||
getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null>;
|
||||
getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtensionVersion[]>;
|
||||
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>;
|
||||
getExtensionsReport(): Promise<IReportedExtension[]>;
|
||||
isExtensionCompatible(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise<boolean>;
|
||||
getCompatibleExtension(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null>;
|
||||
getCompatibleExtension(id: IExtensionIdentifier, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null>;
|
||||
getAllCompatibleVersions(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise<IGalleryExtensionVersion[]>;
|
||||
getExtensionsControlManifest(): Promise<IExtensionsControlManifest>;
|
||||
}
|
||||
|
||||
export interface InstallExtensionEvent {
|
||||
@@ -367,8 +358,12 @@ export interface DidUninstallExtensionEvent {
|
||||
|
||||
export enum ExtensionManagementErrorCode {
|
||||
Unsupported = 'Unsupported',
|
||||
UnsupportedPreRelease = 'UnsupportedPreRelease',
|
||||
Malicious = 'Malicious',
|
||||
Incompatible = 'Incompatible',
|
||||
IncompatiblePreRelease = 'IncompatiblePreRelease',
|
||||
IncompatibleTargetPlatform = 'IncompatibleTargetPlatform',
|
||||
ReleaseVersionNotFound = 'ReleaseVersionNotFound',
|
||||
Invalid = 'Invalid',
|
||||
Download = 'Download',
|
||||
Extract = 'Extract',
|
||||
@@ -386,9 +381,9 @@ export class ExtensionManagementError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export type InstallOptions = { isBuiltin?: boolean, isMachineScoped?: boolean, donotIncludePackAndDependencies?: boolean, installGivenVersion?: boolean };
|
||||
export type InstallOptions = { isBuiltin?: boolean; isMachineScoped?: boolean; donotIncludePackAndDependencies?: boolean; installGivenVersion?: boolean; installPreReleaseVersion?: boolean; operation?: InstallOperation };
|
||||
export type InstallVSIXOptions = Omit<InstallOptions, 'installGivenVersion'> & { installOnlyNewlyAddedFromExtensionPack?: boolean };
|
||||
export type UninstallOptions = { donotIncludePack?: boolean, donotCheckDependents?: boolean };
|
||||
export type UninstallOptions = { donotIncludePack?: boolean; donotCheckDependents?: boolean };
|
||||
|
||||
export interface IExtensionManagementParticipant {
|
||||
postInstall(local: ILocalExtension, source: URI | IGalleryExtension, options: InstallOptions | InstallVSIXOptions, token: CancellationToken): Promise<void>;
|
||||
@@ -412,8 +407,8 @@ export interface IExtensionManagementService {
|
||||
installFromGallery(extension: IGalleryExtension, options?: InstallOptions): Promise<ILocalExtension>;
|
||||
uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise<void>;
|
||||
reinstallFromGallery(extension: ILocalExtension): Promise<void>;
|
||||
getInstalled(type?: ExtensionType, donotIgnoreInvalidExtensions?: boolean): Promise<ILocalExtension[]>;
|
||||
getExtensionsReport(): Promise<IReportedExtension[]>;
|
||||
getInstalled(type?: ExtensionType): Promise<ILocalExtension[]>;
|
||||
getExtensionsControlManifest(): Promise<IExtensionsControlManifest>;
|
||||
|
||||
updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension>;
|
||||
updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise<ILocalExtension>;
|
||||
@@ -428,7 +423,7 @@ export const IGlobalExtensionEnablementService = createDecorator<IGlobalExtensio
|
||||
|
||||
export interface IGlobalExtensionEnablementService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly onDidChangeEnablement: Event<{ readonly extensions: IExtensionIdentifier[], readonly source?: string }>;
|
||||
readonly onDidChangeEnablement: Event<{ readonly extensions: IExtensionIdentifier[]; readonly source?: string }>;
|
||||
|
||||
getDisabledExtensions(): IExtensionIdentifier[];
|
||||
enableExtension(extension: IExtensionIdentifier, source?: string): Promise<boolean>;
|
||||
@@ -437,23 +432,25 @@ export interface IGlobalExtensionEnablementService {
|
||||
}
|
||||
|
||||
export type IConfigBasedExtensionTip = {
|
||||
readonly extensionId: string,
|
||||
readonly extensionName: string,
|
||||
readonly isExtensionPack: boolean,
|
||||
readonly configName: string,
|
||||
readonly important: boolean,
|
||||
readonly extensionId: string;
|
||||
readonly extensionName: string;
|
||||
readonly isExtensionPack: boolean;
|
||||
readonly configName: string;
|
||||
readonly important: boolean;
|
||||
readonly whenNotInstalled?: string[];
|
||||
};
|
||||
|
||||
export type IExecutableBasedExtensionTip = {
|
||||
readonly extensionId: string,
|
||||
readonly extensionName: string,
|
||||
readonly isExtensionPack: boolean,
|
||||
readonly exeName: string,
|
||||
readonly exeFriendlyName: string,
|
||||
readonly windowsPath?: string,
|
||||
readonly extensionId: string;
|
||||
readonly extensionName: string;
|
||||
readonly isExtensionPack: boolean;
|
||||
readonly exeName: string;
|
||||
readonly exeFriendlyName: string;
|
||||
readonly windowsPath?: string;
|
||||
readonly whenNotInstalled?: string[];
|
||||
};
|
||||
|
||||
export type IWorkspaceTips = { readonly remoteSet: string[]; readonly recommendations: string[]; };
|
||||
export type IWorkspaceTips = { readonly remoteSet: string[]; readonly recommendations: string[] };
|
||||
|
||||
export const IExtensionTipsService = createDecorator<IExtensionTipsService>('IExtensionTipsService');
|
||||
export interface IExtensionTipsService {
|
||||
@@ -466,7 +463,6 @@ export interface IExtensionTipsService {
|
||||
}
|
||||
|
||||
|
||||
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';
|
||||
@@ -484,7 +480,7 @@ export interface IExtensionManagementCLIService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
listExtensions(showVersions: boolean, category?: string, output?: CLIOutput): Promise<void>;
|
||||
installExtensions(extensions: (string | URI)[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean, output?: CLIOutput): Promise<void>;
|
||||
installExtensions(extensions: (string | URI)[], builtinExtensionIds: string[], installOptions: InstallOptions, force: boolean, output?: CLIOutput): Promise<void>;
|
||||
uninstallExtensions(extensions: (string | URI)[], force: boolean, output?: CLIOutput): Promise<void>;
|
||||
locateExtension(extensions: string[], output?: CLIOutput): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { isPromiseCanceledError } from 'vs/base/common/errors';
|
||||
import { isCancellationError } from 'vs/base/common/errors';
|
||||
import { getBaseLabel } from 'vs/base/common/labels';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { gt } from 'vs/base/common/semver/semver';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { localize } from 'vs/nls';
|
||||
import { CLIOutput, IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { areSameExtensions, getGalleryExtensionId, getIdAndVersion } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { ExtensionType, EXTENSION_CATEGORIES, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
|
||||
@@ -27,17 +27,7 @@ function getId(manifest: IExtensionManifest, withVersion?: boolean): string {
|
||||
}
|
||||
}
|
||||
|
||||
const EXTENSION_ID_REGEX = /^([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/;
|
||||
|
||||
export function getIdAndVersion(id: string): [string, string | undefined] {
|
||||
const matches = EXTENSION_ID_REGEX.exec(id);
|
||||
if (matches && matches[1]) {
|
||||
return [adoptToGalleryExtensionId(matches[1]), matches[2]];
|
||||
}
|
||||
return [adoptToGalleryExtensionId(id), undefined];
|
||||
}
|
||||
|
||||
type InstallExtensionInfo = { id: string, version?: string, installOptions: InstallOptions };
|
||||
type InstallExtensionInfo = { id: string; version?: string; installOptions: InstallOptions };
|
||||
|
||||
|
||||
export class ExtensionManagementCLIService implements IExtensionManagementCLIService {
|
||||
@@ -89,7 +79,7 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
|
||||
}
|
||||
}
|
||||
|
||||
public async installExtensions(extensions: (string | URI)[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean, output: CLIOutput = console): Promise<void> {
|
||||
public async installExtensions(extensions: (string | URI)[], builtinExtensionIds: string[], installOptions: InstallOptions, force: boolean, output: CLIOutput = console): Promise<void> {
|
||||
const failed: string[] = [];
|
||||
const installedExtensionsManifests: IExtensionManifest[] = [];
|
||||
if (extensions.length) {
|
||||
@@ -100,7 +90,7 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
|
||||
const checkIfNotInstalled = (id: string, version?: string): boolean => {
|
||||
const installedExtension = installed.find(i => areSameExtensions(i.identifier, { id }));
|
||||
if (installedExtension) {
|
||||
if (!version && !force) {
|
||||
if (!force && (!version || (version === 'prerelease' && installedExtension.preRelease))) {
|
||||
output.log(localize('alreadyInstalled-checkAndUpdate', "Extension '{0}' v{1} is already installed. Use '--force' option to update to latest version or provide '@<version>' to install a specific version, for example: '{2}@1.2.3'.", id, installedExtension.manifest.version, id));
|
||||
return false;
|
||||
}
|
||||
@@ -111,6 +101,9 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const addInstallExtensionInfo = (id: string, version: string | undefined, isBuiltin: boolean) => {
|
||||
installExtensionInfos.push({ id, version: version !== 'prerelease' ? version : undefined, installOptions: { ...installOptions, isBuiltin, installPreReleaseVersion: version === 'prerelease' || installOptions.installPreReleaseVersion } });
|
||||
};
|
||||
const vsixs: URI[] = [];
|
||||
const installExtensionInfos: InstallExtensionInfo[] = [];
|
||||
for (const extension of extensions) {
|
||||
@@ -119,21 +112,21 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
|
||||
} else {
|
||||
const [id, version] = getIdAndVersion(extension);
|
||||
if (checkIfNotInstalled(id, version)) {
|
||||
installExtensionInfos.push({ id, version, installOptions: { isBuiltin: false, isMachineScoped } });
|
||||
addInstallExtensionInfo(id, version, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const extension of builtinExtensionIds) {
|
||||
const [id, version] = getIdAndVersion(extension);
|
||||
if (checkIfNotInstalled(id, version)) {
|
||||
installExtensionInfos.push({ id, version, installOptions: { isBuiltin: true, isMachineScoped: false } });
|
||||
addInstallExtensionInfo(id, version, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (vsixs.length) {
|
||||
await Promise.all(vsixs.map(async vsix => {
|
||||
try {
|
||||
const manifest = await this.installVSIX(vsix, { isBuiltin: false, isMachineScoped }, force, output);
|
||||
const manifest = await this.installVSIX(vsix, { ...installOptions, isBuiltin: false }, force, output);
|
||||
if (manifest) {
|
||||
installedExtensionsManifests.push(manifest);
|
||||
}
|
||||
@@ -187,7 +180,7 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
|
||||
output.log(localize('successVsixInstall', "Extension '{0}' was successfully installed.", getBaseLabel(vsix)));
|
||||
return manifest;
|
||||
} catch (error) {
|
||||
if (isPromiseCanceledError(error)) {
|
||||
if (isCancellationError(error)) {
|
||||
output.log(localize('cancelVsixInstall', "Cancelled installing extension '{0}'.", getBaseLabel(vsix)));
|
||||
return null;
|
||||
} else {
|
||||
@@ -200,7 +193,8 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
|
||||
|
||||
private async getGalleryExtensions(extensions: InstallExtensionInfo[]): Promise<Map<string, IGalleryExtension>> {
|
||||
const galleryExtensions = new Map<string, IGalleryExtension>();
|
||||
const result = await this.extensionGalleryService.getExtensions(extensions, CancellationToken.None);
|
||||
const preRelease = extensions.some(e => e.installOptions.installPreReleaseVersion);
|
||||
const result = await this.extensionGalleryService.getExtensions(extensions.map(e => ({ ...e, preRelease })), CancellationToken.None);
|
||||
for (const extension of result) {
|
||||
galleryExtensions.set(extension.identifier.id.toLowerCase(), extension);
|
||||
}
|
||||
@@ -233,7 +227,7 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
|
||||
output.log(localize('successInstall', "Extension '{0}' v{1} was successfully installed.", id, galleryExtension.version));
|
||||
return manifest;
|
||||
} catch (error) {
|
||||
if (isPromiseCanceledError(error)) {
|
||||
if (isCancellationError(error)) {
|
||||
output.log(localize('cancelInstall', "Cancelled installing extension '{0}'.", id));
|
||||
return null;
|
||||
} else {
|
||||
|
||||
@@ -9,8 +9,8 @@ import { cloneAndChange } from 'vs/base/common/objects';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc';
|
||||
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { DidUninstallExtensionEvent, IExtensionIdentifier, IExtensionManagementService, IExtensionTipsService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, InstallOptions, InstallVSIXOptions, IReportedExtension, isTargetPlatformCompatible, TargetPlatform, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
|
||||
import { DidUninstallExtensionEvent, IExtensionIdentifier, IExtensionManagementService, IExtensionTipsService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, InstallOptions, InstallVSIXOptions, IExtensionsControlManifest, isTargetPlatformCompatible, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI {
|
||||
return URI.revive(transformer ? transformer.transformIncoming(uri) : uri);
|
||||
@@ -72,7 +72,7 @@ export class ExtensionManagementChannel implements IServerChannel {
|
||||
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();
|
||||
case 'getExtensionsControlManifest': return this.service.getExtensionsControlManifest();
|
||||
}
|
||||
|
||||
throw new Error('Invalid call');
|
||||
@@ -113,7 +113,7 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt
|
||||
typeof (<any>thing).scheme === 'string';
|
||||
}
|
||||
|
||||
private _targetPlatformPromise: Promise<TargetPlatform> | undefined;
|
||||
protected _targetPlatformPromise: Promise<TargetPlatform> | undefined;
|
||||
getTargetPlatform(): Promise<TargetPlatform> {
|
||||
if (!this._targetPlatformPromise) {
|
||||
this._targetPlatformPromise = this.channel.call<TargetPlatform>('getTargetPlatform');
|
||||
@@ -169,8 +169,8 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt
|
||||
.then(extension => transformIncomingExtension(extension, null));
|
||||
}
|
||||
|
||||
getExtensionsReport(): Promise<IReportedExtension[]> {
|
||||
return Promise.resolve(this.channel.call('getExtensionsReport'));
|
||||
getExtensionsControlManifest(): Promise<IExtensionsControlManifest> {
|
||||
return Promise.resolve(this.channel.call('getExtensionsControlManifest'));
|
||||
}
|
||||
|
||||
registerParticipant() { throw new Error('Not Supported'); }
|
||||
|
||||
@@ -4,8 +4,14 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { compareIgnoreCase } from 'vs/base/common/strings';
|
||||
import { IExtensionIdentifier, IExtensionIdentifierWithVersion, IGalleryExtension, ILocalExtension, IReportedExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { ExtensionIdentifier, IExtension } from 'vs/platform/extensions/common/extensions';
|
||||
import { IExtensionIdentifier, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, getTargetPlatform } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { ExtensionIdentifier, IExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { isLinux, platform } from 'vs/base/common/platform';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { arch } from 'vs/base/common/process';
|
||||
|
||||
export function areSameExtensions(a: IExtensionIdentifier, b: IExtensionIdentifier): boolean {
|
||||
if (a.uuid && b.uuid) {
|
||||
@@ -17,31 +23,52 @@ export function areSameExtensions(a: IExtensionIdentifier, b: IExtensionIdentifi
|
||||
return compareIgnoreCase(a.id, b.id) === 0;
|
||||
}
|
||||
|
||||
export class ExtensionIdentifierWithVersion implements IExtensionIdentifierWithVersion {
|
||||
const ExtensionKeyRegex = /^([^.]+\..+)-(\d+\.\d+\.\d+)(-(.+))?$/;
|
||||
|
||||
export class ExtensionKey {
|
||||
|
||||
static create(extension: IExtension | IGalleryExtension): ExtensionKey {
|
||||
const version = (extension as IExtension).manifest ? (extension as IExtension).manifest.version : (extension as IGalleryExtension).version;
|
||||
const targetPlatform = (extension as IExtension).manifest ? (extension as IExtension).targetPlatform : (extension as IGalleryExtension).properties.targetPlatform;
|
||||
return new ExtensionKey(extension.identifier, version, targetPlatform);
|
||||
}
|
||||
|
||||
static parse(key: string): ExtensionKey | null {
|
||||
const matches = ExtensionKeyRegex.exec(key);
|
||||
return matches && matches[1] && matches[2] ? new ExtensionKey({ id: matches[1] }, matches[2], matches[4] as TargetPlatform || undefined) : null;
|
||||
}
|
||||
|
||||
readonly id: string;
|
||||
readonly uuid?: string;
|
||||
|
||||
constructor(
|
||||
identifier: IExtensionIdentifier,
|
||||
readonly version: string
|
||||
readonly version: string,
|
||||
readonly targetPlatform: TargetPlatform = TargetPlatform.UNDEFINED,
|
||||
) {
|
||||
this.id = identifier.id;
|
||||
this.uuid = identifier.uuid;
|
||||
}
|
||||
|
||||
key(): string {
|
||||
return `${this.id}-${this.version}`;
|
||||
toString(): string {
|
||||
return `${this.id}-${this.version}${this.targetPlatform !== TargetPlatform.UNDEFINED ? `-${this.targetPlatform}` : ''}`;
|
||||
}
|
||||
|
||||
equals(o: any): boolean {
|
||||
if (!(o instanceof ExtensionIdentifierWithVersion)) {
|
||||
if (!(o instanceof ExtensionKey)) {
|
||||
return false;
|
||||
}
|
||||
return areSameExtensions(this, o) && this.version === o.version;
|
||||
return areSameExtensions(this, o) && this.version === o.version && this.targetPlatform === o.targetPlatform;
|
||||
}
|
||||
}
|
||||
|
||||
const EXTENSION_IDENTIFIER_WITH_VERSION_REGEX = /^([^.]+\..+)@((prerelease)|(\d+\.\d+\.\d+(-.*)?))$/;
|
||||
export function getIdAndVersion(id: string): [string, string | undefined] {
|
||||
const matches = EXTENSION_IDENTIFIER_WITH_VERSION_REGEX.exec(id);
|
||||
if (matches && matches[1]) {
|
||||
return [adoptToGalleryExtensionId(matches[1]), matches[2]];
|
||||
}
|
||||
return [adoptToGalleryExtensionId(id), undefined];
|
||||
}
|
||||
|
||||
export function getExtensionId(publisher: string, name: string): string {
|
||||
return `${publisher}.${name}`;
|
||||
}
|
||||
@@ -97,6 +124,7 @@ export function getLocalExtensionTelemetryData(extension: ILocalExtension): any
|
||||
"publisherId": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"publisherName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"publisherDisplayName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"isPreReleaseVersion": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"dependencies": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
|
||||
"${include}": [
|
||||
"${GalleryExtensionTelemetryData2}"
|
||||
@@ -111,6 +139,7 @@ export function getGalleryExtensionTelemetryData(extension: IGalleryExtension):
|
||||
publisherId: extension.publisherId,
|
||||
publisherName: extension.publisher,
|
||||
publisherDisplayName: extension.publisherDisplayName,
|
||||
isPreReleaseVersion: extension.properties.isPreReleaseVersion,
|
||||
dependencies: !!(extension.properties.dependencies && extension.properties.dependencies.length > 0),
|
||||
// {{SQL CARBON EDIT}}
|
||||
extensionVersion: extension.version,
|
||||
@@ -120,12 +149,12 @@ export function getGalleryExtensionTelemetryData(extension: IGalleryExtension):
|
||||
|
||||
export const BetterMergeId = new ExtensionIdentifier('pprice.better-merge');
|
||||
|
||||
export function getMaliciousExtensionsSet(report: IReportedExtension[]): Set<string> {
|
||||
export function getMaliciousExtensionsSet(manifest: IExtensionsControlManifest): Set<string> {
|
||||
const result = new Set<string>();
|
||||
|
||||
for (const extension of report) {
|
||||
if (extension.malicious) {
|
||||
result.add(extension.id.id);
|
||||
if (manifest.malicious) {
|
||||
for (const extension of manifest.malicious) {
|
||||
result.add(extension.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,3 +179,30 @@ export function getExtensionDependencies(installedExtensions: ReadonlyArray<IExt
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
export async function isAlpineLinux(fileService: IFileService, logService: ILogService): Promise<boolean> {
|
||||
if (!isLinux) {
|
||||
return false;
|
||||
}
|
||||
let content: string | undefined;
|
||||
try {
|
||||
const fileContent = await fileService.readFile(URI.file('/etc/os-release'));
|
||||
content = fileContent.value.toString();
|
||||
} catch (error) {
|
||||
try {
|
||||
const fileContent = await fileService.readFile(URI.file('/usr/lib/os-release'));
|
||||
content = fileContent.value.toString();
|
||||
} catch (error) {
|
||||
/* Ignore */
|
||||
logService.debug(`Error while getting the os-release file.`, getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
return !!content && (content.match(/^ID=([^\u001b\r\n]*)/m) || [])[1] === 'alpine';
|
||||
}
|
||||
|
||||
export async function computeTargetPlatform(fileService: IFileService, logService: ILogService): Promise<TargetPlatform> {
|
||||
const alpineLinux = await isAlpineLinux(fileService, logService);
|
||||
const targetPlatform = getTargetPlatform(alpineLinux ? 'alpine' : platform, arch);
|
||||
logService.debug('ComputeTargetPlatform:', targetPlatform);
|
||||
return targetPlatform;
|
||||
}
|
||||
|
||||
214
src/vs/platform/extensionManagement/common/extensionStorage.ts
Normal file
214
src/vs/platform/extensionManagement/common/extensionStorage.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
import { adoptToGalleryExtensionId, areSameExtensions, getExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { distinct } from 'vs/base/common/arrays';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IExtension } from 'vs/platform/extensions/common/extensions';
|
||||
import { isArray, isString } from 'vs/base/common/types';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { IExtensionManagementService, IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
|
||||
export interface IExtensionIdWithVersion {
|
||||
id: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export const IExtensionStorageService = createDecorator<IExtensionStorageService>('IExtensionStorageService');
|
||||
|
||||
export interface IExtensionStorageService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
getExtensionState(extension: IExtension | IGalleryExtension | string, global: boolean): IStringDictionary<any> | undefined;
|
||||
setExtensionState(extension: IExtension | IGalleryExtension | string, state: IStringDictionary<any> | undefined, global: boolean): void;
|
||||
|
||||
readonly onDidChangeExtensionStorageToSync: Event<void>;
|
||||
setKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion, keys: string[]): void;
|
||||
getKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion): string[] | undefined;
|
||||
|
||||
addToMigrationList(from: string, to: string): void;
|
||||
getSourceExtensionToMigrate(target: string): string | undefined;
|
||||
}
|
||||
|
||||
const EXTENSION_KEYS_ID_VERSION_REGEX = /^extensionKeys\/([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/;
|
||||
|
||||
export class ExtensionStorageService extends Disposable implements IExtensionStorageService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
private static toKey(extension: IExtensionIdWithVersion): string {
|
||||
return `extensionKeys/${adoptToGalleryExtensionId(extension.id)}@${extension.version}`;
|
||||
}
|
||||
|
||||
private static fromKey(key: string): IExtensionIdWithVersion | undefined {
|
||||
const matches = EXTENSION_KEYS_ID_VERSION_REGEX.exec(key);
|
||||
if (matches && matches[1]) {
|
||||
return { id: matches[1], version: matches[2] };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static async removeOutdatedExtensionVersions(extensionManagementService: IExtensionManagementService, storageService: IStorageService): Promise<void> {
|
||||
const extensions = await extensionManagementService.getInstalled();
|
||||
const extensionVersionsToRemove: string[] = [];
|
||||
for (const [id, versions] of ExtensionStorageService.readAllExtensionsWithKeysForSync(storageService)) {
|
||||
const extensionVersion = extensions.find(e => areSameExtensions(e.identifier, { id }))?.manifest.version;
|
||||
for (const version of versions) {
|
||||
if (extensionVersion !== version) {
|
||||
extensionVersionsToRemove.push(ExtensionStorageService.toKey({ id, version }));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const key of extensionVersionsToRemove) {
|
||||
storageService.remove(key, StorageScope.GLOBAL);
|
||||
}
|
||||
}
|
||||
|
||||
private static readAllExtensionsWithKeysForSync(storageService: IStorageService): Map<string, string[]> {
|
||||
const extensionsWithKeysForSync = new Map<string, string[]>();
|
||||
const keys = storageService.keys(StorageScope.GLOBAL, StorageTarget.MACHINE);
|
||||
for (const key of keys) {
|
||||
const extensionIdWithVersion = ExtensionStorageService.fromKey(key);
|
||||
if (extensionIdWithVersion) {
|
||||
let versions = extensionsWithKeysForSync.get(extensionIdWithVersion.id.toLowerCase());
|
||||
if (!versions) {
|
||||
extensionsWithKeysForSync.set(extensionIdWithVersion.id.toLowerCase(), versions = []);
|
||||
}
|
||||
versions.push(extensionIdWithVersion.version);
|
||||
}
|
||||
}
|
||||
return extensionsWithKeysForSync;
|
||||
}
|
||||
|
||||
private readonly _onDidChangeExtensionStorageToSync = this._register(new Emitter<void>());
|
||||
readonly onDidChangeExtensionStorageToSync = this._onDidChangeExtensionStorageToSync.event;
|
||||
|
||||
private readonly extensionsWithKeysForSync: Map<string, string[]>;
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
) {
|
||||
super();
|
||||
this.extensionsWithKeysForSync = ExtensionStorageService.readAllExtensionsWithKeysForSync(storageService);
|
||||
this._register(this.storageService.onDidChangeValue(e => this.onDidChangeStorageValue(e)));
|
||||
}
|
||||
|
||||
private onDidChangeStorageValue(e: IStorageValueChangeEvent): void {
|
||||
if (e.scope !== StorageScope.GLOBAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// State of extension with keys for sync has changed
|
||||
if (this.extensionsWithKeysForSync.has(e.key.toLowerCase())) {
|
||||
this._onDidChangeExtensionStorageToSync.fire();
|
||||
return;
|
||||
}
|
||||
|
||||
// Keys for sync of an extension has changed
|
||||
const extensionIdWithVersion = ExtensionStorageService.fromKey(e.key);
|
||||
if (extensionIdWithVersion) {
|
||||
if (this.storageService.get(e.key, StorageScope.GLOBAL) === undefined) {
|
||||
this.extensionsWithKeysForSync.delete(extensionIdWithVersion.id.toLowerCase());
|
||||
} else {
|
||||
let versions = this.extensionsWithKeysForSync.get(extensionIdWithVersion.id.toLowerCase());
|
||||
if (!versions) {
|
||||
this.extensionsWithKeysForSync.set(extensionIdWithVersion.id.toLowerCase(), versions = []);
|
||||
}
|
||||
versions.push(extensionIdWithVersion.version);
|
||||
this._onDidChangeExtensionStorageToSync.fire();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private getExtensionId(extension: IExtension | IGalleryExtension | string): string {
|
||||
if (isString(extension)) {
|
||||
return extension;
|
||||
}
|
||||
const publisher = (extension as IExtension).manifest ? (extension as IExtension).manifest.publisher : (extension as IGalleryExtension).publisher;
|
||||
const name = (extension as IExtension).manifest ? (extension as IExtension).manifest.name : (extension as IGalleryExtension).name;
|
||||
return getExtensionId(publisher, name);
|
||||
}
|
||||
|
||||
getExtensionState(extension: IExtension | IGalleryExtension | string, global: boolean): IStringDictionary<any> | undefined {
|
||||
const extensionId = this.getExtensionId(extension);
|
||||
const jsonValue = this.storageService.get(extensionId, global ? StorageScope.GLOBAL : StorageScope.WORKSPACE);
|
||||
if (jsonValue) {
|
||||
try {
|
||||
return JSON.parse(jsonValue);
|
||||
} catch (error) {
|
||||
// Do not fail this call but log it for diagnostics
|
||||
// https://github.com/microsoft/vscode/issues/132777
|
||||
this.logService.error(`[mainThreadStorage] unexpected error parsing storage contents (extensionId: ${extensionId}, global: ${global}): ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
setExtensionState(extension: IExtension | IGalleryExtension | string, state: IStringDictionary<any> | undefined, global: boolean): void {
|
||||
const extensionId = this.getExtensionId(extension);
|
||||
if (state === undefined) {
|
||||
this.storageService.remove(extensionId, global ? StorageScope.GLOBAL : StorageScope.WORKSPACE);
|
||||
} else {
|
||||
this.storageService.store(extensionId, JSON.stringify(state), global ? StorageScope.GLOBAL : StorageScope.WORKSPACE, StorageTarget.MACHINE /* Extension state is synced separately through extensions */);
|
||||
}
|
||||
}
|
||||
|
||||
setKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion, keys: string[]): void {
|
||||
this.storageService.store(ExtensionStorageService.toKey(extensionIdWithVersion), JSON.stringify(keys), StorageScope.GLOBAL, StorageTarget.MACHINE);
|
||||
}
|
||||
|
||||
getKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion): string[] | undefined {
|
||||
const extensionKeysForSyncFromProduct = this.productService.extensionSyncedKeys?.[extensionIdWithVersion.id.toLowerCase()];
|
||||
const extensionKeysForSyncFromStorageValue = this.storageService.get(ExtensionStorageService.toKey(extensionIdWithVersion), StorageScope.GLOBAL);
|
||||
const extensionKeysForSyncFromStorage = extensionKeysForSyncFromStorageValue ? JSON.parse(extensionKeysForSyncFromStorageValue) : undefined;
|
||||
|
||||
return extensionKeysForSyncFromStorage && extensionKeysForSyncFromProduct
|
||||
? distinct([...extensionKeysForSyncFromStorage, ...extensionKeysForSyncFromProduct])
|
||||
: (extensionKeysForSyncFromStorage || extensionKeysForSyncFromProduct);
|
||||
}
|
||||
|
||||
addToMigrationList(from: string, to: string): void {
|
||||
if (from !== to) {
|
||||
// remove the duplicates
|
||||
const migrationList: [string, string][] = this.migrationList.filter(entry => !entry.includes(from) && !entry.includes(to));
|
||||
migrationList.push([from, to]);
|
||||
this.migrationList = migrationList;
|
||||
}
|
||||
}
|
||||
|
||||
getSourceExtensionToMigrate(toExtensionId: string): string | undefined {
|
||||
const entry = this.migrationList.find(([, to]) => toExtensionId === to);
|
||||
return entry ? entry[0] : undefined;
|
||||
}
|
||||
|
||||
private get migrationList(): [string, string][] {
|
||||
const value = this.storageService.get('extensionStorage.migrationList', StorageScope.GLOBAL, '[]');
|
||||
try {
|
||||
const migrationList = JSON.parse(value);
|
||||
if (isArray(migrationList)) {
|
||||
return migrationList;
|
||||
}
|
||||
} catch (error) { /* ignore */ }
|
||||
return [];
|
||||
}
|
||||
|
||||
private set migrationList(migrationList: [string, string][]) {
|
||||
if (migrationList.length) {
|
||||
this.storageService.store('extensionStorage.migrationList', JSON.stringify(migrationList), StorageScope.GLOBAL, StorageTarget.MACHINE);
|
||||
} else {
|
||||
this.storageService.remove('extensionStorage.migrationList', StorageScope.GLOBAL);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -68,7 +68,8 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
|
||||
extensionName: value.name,
|
||||
configName: tip.configName,
|
||||
important: !!value.important,
|
||||
isExtensionPack: !!value.isExtensionPack
|
||||
isExtensionPack: !!value.isExtensionPack,
|
||||
whenNotInstalled: value.whenNotInstalled
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -77,7 +78,8 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
|
||||
extensionName: value.name,
|
||||
configName: tip.configName,
|
||||
important: !!value.important,
|
||||
isExtensionPack: !!value.isExtensionPack
|
||||
isExtensionPack: !!value.isExtensionPack,
|
||||
whenNotInstalled: value.whenNotInstalled
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,875 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { ThrottledDelayer } from 'vs/base/common/async';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
import { getNodeType, parse, ParseError } from 'vs/base/common/json';
|
||||
import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { FileAccess, Schemas } from 'vs/base/common/network';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { basename, isEqual, joinPath } from 'vs/base/common/resources';
|
||||
import * as semver from 'vs/base/common/semver/semver';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { isArray, isObject, isString } from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { Metadata } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { areSameExtensions, computeTargetPlatform, ExtensionKey, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription, BUILTIN_MANIFEST_CACHE_FILE, USER_MANIFEST_CACHE_FILE, MANIFEST_CACHE_FOLDER } from 'vs/platform/extensions/common/extensions';
|
||||
import { validateExtensionManifest } from 'vs/platform/extensions/common/extensionValidator';
|
||||
import { FileOperationResult, IFileService, toFileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { revive } from 'vs/base/common/marshalling';
|
||||
|
||||
export type IScannedExtensionManifest = IRelaxedExtensionManifest & { __metadata?: Metadata };
|
||||
|
||||
interface IRelaxedScannedExtension {
|
||||
type: ExtensionType;
|
||||
isBuiltin: boolean;
|
||||
identifier: IExtensionIdentifier;
|
||||
manifest: IRelaxedExtensionManifest;
|
||||
location: URI;
|
||||
targetPlatform: TargetPlatform;
|
||||
metadata: Metadata | undefined;
|
||||
isValid: boolean;
|
||||
validations: readonly [Severity, string][];
|
||||
}
|
||||
|
||||
export type IScannedExtension = Readonly<IRelaxedScannedExtension> & { manifest: IExtensionManifest };
|
||||
|
||||
export interface Translations {
|
||||
[id: string]: string;
|
||||
}
|
||||
|
||||
export namespace Translations {
|
||||
export function equals(a: Translations, b: Translations): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
let aKeys = Object.keys(a);
|
||||
let bKeys: Set<string> = new Set<string>();
|
||||
for (let key of Object.keys(b)) {
|
||||
bKeys.add(key);
|
||||
}
|
||||
if (aKeys.length !== bKeys.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let key of aKeys) {
|
||||
if (a[key] !== b[key]) {
|
||||
return false;
|
||||
}
|
||||
bKeys.delete(key);
|
||||
}
|
||||
return bKeys.size === 0;
|
||||
}
|
||||
}
|
||||
|
||||
interface MessageBag {
|
||||
[key: string]: string | { message: string; comment: string[] };
|
||||
}
|
||||
|
||||
interface TranslationBundle {
|
||||
contents: {
|
||||
package: MessageBag;
|
||||
};
|
||||
}
|
||||
|
||||
interface LocalizedMessages {
|
||||
values: MessageBag | undefined;
|
||||
default: URI | null;
|
||||
}
|
||||
|
||||
interface IBuiltInExtensionControl {
|
||||
[name: string]: 'marketplace' | 'disabled' | string;
|
||||
}
|
||||
|
||||
export type ScanOptions = {
|
||||
readonly includeInvalid?: boolean;
|
||||
readonly includeAllVersions?: boolean;
|
||||
readonly includeUninstalled?: boolean;
|
||||
readonly checkControlFile?: boolean;
|
||||
readonly language?: string;
|
||||
readonly useCache?: boolean;
|
||||
};
|
||||
|
||||
export const IExtensionsScannerService = createDecorator<IExtensionsScannerService>('IExtensionsScannerService');
|
||||
export interface IExtensionsScannerService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly systemExtensionsLocation: URI;
|
||||
readonly userExtensionsLocation: URI;
|
||||
readonly onDidChangeCache: Event<ExtensionType>;
|
||||
|
||||
getTargetPlatform(): Promise<TargetPlatform>;
|
||||
|
||||
scanAllExtensions(scanOptions: ScanOptions): Promise<IScannedExtension[]>;
|
||||
scanSystemExtensions(scanOptions: ScanOptions): Promise<IScannedExtension[]>;
|
||||
scanUserExtensions(scanOptions: ScanOptions): Promise<IScannedExtension[]>;
|
||||
scanExtensionsUnderDevelopment(scanOptions: ScanOptions, existingExtensions: IScannedExtension[]): Promise<IScannedExtension[]>;
|
||||
scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise<IScannedExtension | null>;
|
||||
scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise<IScannedExtension[]>;
|
||||
|
||||
updateMetadata(extensionLocation: URI, metadata: Partial<Metadata>): Promise<void>;
|
||||
}
|
||||
|
||||
export abstract class AbstractExtensionsScannerService extends Disposable implements IExtensionsScannerService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
protected abstract getTranslations(language: string): Promise<Translations>;
|
||||
|
||||
private readonly _onDidChangeCache = this._register(new Emitter<ExtensionType>());
|
||||
readonly onDidChangeCache = this._onDidChangeCache.event;
|
||||
|
||||
private readonly systemExtensionsCachedScanner = this._register(new CachedExtensionsScanner(joinPath(this.cacheLocation, BUILTIN_MANIFEST_CACHE_FILE), this.fileService, this.logService));
|
||||
private readonly userExtensionsCachedScanner = this._register(new CachedExtensionsScanner(joinPath(this.cacheLocation, USER_MANIFEST_CACHE_FILE), this.fileService, this.logService));
|
||||
private readonly extensionsScanner = this._register(new ExtensionsScanner(this.fileService, this.logService));
|
||||
|
||||
constructor(
|
||||
readonly systemExtensionsLocation: URI,
|
||||
readonly userExtensionsLocation: URI,
|
||||
private readonly extensionsControlLocation: URI,
|
||||
private readonly cacheLocation: URI,
|
||||
@IFileService protected readonly fileService: IFileService,
|
||||
@ILogService protected readonly logService: ILogService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(this.systemExtensionsCachedScanner.onDidChangeCache(() => this._onDidChangeCache.fire(ExtensionType.System)));
|
||||
this._register(this.userExtensionsCachedScanner.onDidChangeCache(() => this._onDidChangeCache.fire(ExtensionType.User)));
|
||||
}
|
||||
|
||||
private _targetPlatformPromise: Promise<TargetPlatform> | undefined;
|
||||
getTargetPlatform(): Promise<TargetPlatform> {
|
||||
if (!this._targetPlatformPromise) {
|
||||
this._targetPlatformPromise = computeTargetPlatform(this.fileService, this.logService);
|
||||
}
|
||||
return this._targetPlatformPromise;
|
||||
}
|
||||
|
||||
async scanAllExtensions(scanOptions: ScanOptions): Promise<IScannedExtension[]> {
|
||||
const [system, user] = await Promise.all([
|
||||
this.scanSystemExtensions(scanOptions),
|
||||
this.scanUserExtensions(scanOptions),
|
||||
]);
|
||||
const development = await this.scanExtensionsUnderDevelopment(scanOptions, [...system, ...user]);
|
||||
return this.dedupExtensions([...system, ...user, ...development], await this.getTargetPlatform(), true);
|
||||
}
|
||||
|
||||
async scanSystemExtensions(scanOptions: ScanOptions): Promise<IScannedExtension[]> {
|
||||
const promises: Promise<IRelaxedScannedExtension[]>[] = [];
|
||||
promises.push(this.scanDefaultSystemExtensions(!!scanOptions.useCache, scanOptions.language));
|
||||
promises.push(this.scanDevSystemExtensions(scanOptions.language, !!scanOptions.checkControlFile));
|
||||
const [defaultSystemExtensions, devSystemExtensions] = await Promise.all(promises);
|
||||
return this.applyScanOptions([...defaultSystemExtensions, ...devSystemExtensions], scanOptions, false);
|
||||
}
|
||||
|
||||
async scanUserExtensions(scanOptions: ScanOptions): Promise<IScannedExtension[]> {
|
||||
this.logService.trace('Started scanning user extensions');
|
||||
const extensionsScannerInput = await this.createExtensionScannerInput(this.userExtensionsLocation, ExtensionType.User, !scanOptions.includeUninstalled, scanOptions.language);
|
||||
const extensionsScanner = scanOptions.useCache && !extensionsScannerInput.devMode && extensionsScannerInput.excludeObsolete ? this.userExtensionsCachedScanner : this.extensionsScanner;
|
||||
let extensions = await extensionsScanner.scanExtensions(extensionsScannerInput);
|
||||
extensions = await this.applyScanOptions(extensions, scanOptions, true);
|
||||
this.logService.trace('Scanned user extensions:', extensions.length);
|
||||
return extensions;
|
||||
}
|
||||
|
||||
async scanExtensionsUnderDevelopment(scanOptions: ScanOptions, existingExtensions: IScannedExtension[]): Promise<IScannedExtension[]> {
|
||||
if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionDevelopmentLocationURI) {
|
||||
const extensions = (await Promise.all(this.environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file)
|
||||
.map(async extensionDevelopmentLocationURI => {
|
||||
const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, ExtensionType.User, true, scanOptions.language, false /* do not validate */);
|
||||
const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(input);
|
||||
return extensions.map(extension => {
|
||||
// Override the extension type from the existing extensions
|
||||
extension.type = existingExtensions.find(e => areSameExtensions(e.identifier, extension.identifier))?.type ?? extension.type;
|
||||
// Validate the extension
|
||||
return this.extensionsScanner.validate(extension, input);
|
||||
});
|
||||
})))
|
||||
.flat();
|
||||
return this.applyScanOptions(extensions, scanOptions, true);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise<IScannedExtension | null> {
|
||||
const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, extensionType, true, scanOptions.language);
|
||||
const extension = await this.extensionsScanner.scanExtension(extensionsScannerInput);
|
||||
if (!extension) {
|
||||
return null;
|
||||
}
|
||||
if (!scanOptions.includeInvalid && !extension.isValid) {
|
||||
return null;
|
||||
}
|
||||
return extension;
|
||||
}
|
||||
|
||||
async scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise<IScannedExtension[]> {
|
||||
const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, extensionType, true, scanOptions.language);
|
||||
const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(extensionsScannerInput);
|
||||
return this.applyScanOptions(extensions, scanOptions, true);
|
||||
}
|
||||
|
||||
async updateMetadata(extensionLocation: URI, metaData: Partial<Metadata>): Promise<void> {
|
||||
const manifestLocation = joinPath(extensionLocation, 'package.json');
|
||||
const content = (await this.fileService.readFile(manifestLocation)).value.toString();
|
||||
const manifest: IScannedExtensionManifest = JSON.parse(content);
|
||||
|
||||
// unset if false
|
||||
metaData.isMachineScoped = metaData.isMachineScoped || undefined;
|
||||
metaData.isBuiltin = metaData.isBuiltin || undefined;
|
||||
metaData.installedTimestamp = metaData.installedTimestamp || undefined;
|
||||
manifest.__metadata = { ...manifest.__metadata, ...metaData };
|
||||
|
||||
await this.fileService.writeFile(joinPath(extensionLocation, 'package.json'), VSBuffer.fromString(JSON.stringify(manifest, null, '\t')));
|
||||
}
|
||||
|
||||
private async applyScanOptions(extensions: IRelaxedScannedExtension[], scanOptions: ScanOptions, pickLatest: boolean): Promise<IRelaxedScannedExtension[]> {
|
||||
if (!scanOptions.includeAllVersions) {
|
||||
extensions = this.dedupExtensions(extensions, await this.getTargetPlatform(), pickLatest);
|
||||
}
|
||||
if (!scanOptions.includeInvalid) {
|
||||
extensions = extensions.filter(extension => extension.isValid);
|
||||
}
|
||||
return extensions.sort((a, b) => {
|
||||
const aLastSegment = path.basename(a.location.fsPath);
|
||||
const bLastSegment = path.basename(b.location.fsPath);
|
||||
if (aLastSegment < bLastSegment) {
|
||||
return -1;
|
||||
}
|
||||
if (aLastSegment > bLastSegment) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
private dedupExtensions(extensions: IRelaxedScannedExtension[], targetPlatform: TargetPlatform, pickLatest: boolean): IRelaxedScannedExtension[] {
|
||||
const result = new Map<string, IRelaxedScannedExtension>();
|
||||
for (const extension of extensions) {
|
||||
const extensionKey = ExtensionIdentifier.toKey(extension.identifier.id);
|
||||
const existing = result.get(extensionKey);
|
||||
if (existing) {
|
||||
if (existing.isValid && !extension.isValid) {
|
||||
continue;
|
||||
}
|
||||
if (existing.isValid === extension.isValid) {
|
||||
if (pickLatest && semver.gt(existing.manifest.version, extension.manifest.version)) {
|
||||
this.logService.debug(`Skipping extension ${extension.location.path} with lower version ${extension.manifest.version}.`);
|
||||
continue;
|
||||
}
|
||||
if (semver.eq(existing.manifest.version, extension.manifest.version) && existing.targetPlatform === targetPlatform) {
|
||||
this.logService.debug(`Skipping extension ${extension.location.path} from different target platform ${extension.targetPlatform}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (existing.type === ExtensionType.System) {
|
||||
this.logService.debug(`Overwriting system extension ${existing.location.path} with ${extension.location.path}.`);
|
||||
} else {
|
||||
this.logService.warn(`Overwriting user extension ${existing.location.path} with ${extension.location.path}.`);
|
||||
}
|
||||
}
|
||||
result.set(extensionKey, extension);
|
||||
}
|
||||
return [...result.values()];
|
||||
}
|
||||
|
||||
private async scanDefaultSystemExtensions(useCache: boolean, language: string | undefined): Promise<IRelaxedScannedExtension[]> {
|
||||
this.logService.trace('Started scanning system extensions');
|
||||
const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, ExtensionType.System, true, language);
|
||||
const extensionsScanner = useCache && !extensionsScannerInput.devMode ? this.systemExtensionsCachedScanner : this.extensionsScanner;
|
||||
const result = await extensionsScanner.scanExtensions(extensionsScannerInput);
|
||||
this.logService.trace('Scanned system extensions:', result.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async scanDevSystemExtensions(language: string | undefined, checkControlFile: boolean): Promise<IRelaxedScannedExtension[]> {
|
||||
const devSystemExtensionsList = this.environmentService.isBuilt ? [] : this.productService.builtInExtensions;
|
||||
if (!devSystemExtensionsList?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
this.logService.trace('Started scanning dev system extensions');
|
||||
const builtinExtensionControl = checkControlFile ? await this.getBuiltInExtensionControl() : {};
|
||||
const devSystemExtensionsLocations: URI[] = [];
|
||||
const devSystemExtensionsLocation = URI.file(path.normalize(path.join(FileAccess.asFileUri('', require).fsPath, '..', '.build', 'builtInExtensions')));
|
||||
for (const extension of devSystemExtensionsList) {
|
||||
const controlState = builtinExtensionControl[extension.name] || 'marketplace';
|
||||
switch (controlState) {
|
||||
case 'disabled':
|
||||
break;
|
||||
case 'marketplace':
|
||||
devSystemExtensionsLocations.push(joinPath(devSystemExtensionsLocation, extension.name));
|
||||
break;
|
||||
default:
|
||||
devSystemExtensionsLocations.push(URI.file(controlState));
|
||||
break;
|
||||
}
|
||||
}
|
||||
const result = await Promise.all(devSystemExtensionsLocations.map(async location => this.extensionsScanner.scanExtension((await this.createExtensionScannerInput(location, ExtensionType.System, true, language)))));
|
||||
this.logService.trace('Scanned dev system extensions:', result.length);
|
||||
return coalesce(result);
|
||||
}
|
||||
|
||||
private async getBuiltInExtensionControl(): Promise<IBuiltInExtensionControl> {
|
||||
try {
|
||||
const content = await this.fileService.readFile(this.extensionsControlLocation);
|
||||
return JSON.parse(content.value.toString());
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
private async createExtensionScannerInput(location: URI, type: ExtensionType, excludeObsolete: boolean, language: string | undefined, validate: boolean = true): Promise<ExtensionScannerInput> {
|
||||
const translations = await this.getTranslations(language ?? platform.language);
|
||||
let mtime: number | undefined;
|
||||
try {
|
||||
const folderStat = await this.fileService.stat(location);
|
||||
if (typeof folderStat.mtime === 'number') {
|
||||
mtime = folderStat.mtime;
|
||||
}
|
||||
} catch (err) {
|
||||
// That's ok...
|
||||
}
|
||||
return new ExtensionScannerInput(
|
||||
location,
|
||||
mtime,
|
||||
type,
|
||||
excludeObsolete,
|
||||
validate,
|
||||
this.productService.version,
|
||||
this.productService.date,
|
||||
this.productService.commit,
|
||||
!this.environmentService.isBuilt,
|
||||
language,
|
||||
translations,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ExtensionScannerInput {
|
||||
|
||||
constructor(
|
||||
public readonly location: URI,
|
||||
public readonly mtime: number | undefined,
|
||||
public readonly type: ExtensionType,
|
||||
public readonly excludeObsolete: boolean,
|
||||
public readonly validate: boolean,
|
||||
public readonly productVersion: string,
|
||||
public readonly productDate: string | undefined,
|
||||
public readonly productCommit: string | undefined,
|
||||
public readonly devMode: boolean,
|
||||
public readonly language: string | undefined,
|
||||
public readonly translations: Translations
|
||||
) {
|
||||
// Keep empty!! (JSON.parse)
|
||||
}
|
||||
|
||||
public static createNlsConfiguration(input: ExtensionScannerInput): NlsConfiguration {
|
||||
return {
|
||||
language: input.language,
|
||||
pseudo: input.language === 'pseudo',
|
||||
devMode: input.devMode,
|
||||
translations: input.translations
|
||||
};
|
||||
}
|
||||
|
||||
public static equals(a: ExtensionScannerInput, b: ExtensionScannerInput): boolean {
|
||||
return (
|
||||
isEqual(a.location, b.location)
|
||||
&& a.mtime === b.mtime
|
||||
&& a.type === b.type
|
||||
&& a.excludeObsolete === b.excludeObsolete
|
||||
&& a.validate === b.validate
|
||||
&& a.productVersion === b.productVersion
|
||||
&& a.productDate === b.productDate
|
||||
&& a.productCommit === b.productCommit
|
||||
&& a.devMode === b.devMode
|
||||
&& a.language === b.language
|
||||
&& Translations.equals(a.translations, b.translations)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type NlsConfiguration = {
|
||||
language: string | undefined;
|
||||
pseudo: boolean;
|
||||
devMode: boolean;
|
||||
translations: Translations;
|
||||
};
|
||||
|
||||
class ExtensionsScanner extends Disposable {
|
||||
|
||||
constructor(
|
||||
protected readonly fileService: IFileService,
|
||||
protected readonly logService: ILogService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async scanExtensions(input: ExtensionScannerInput): Promise<IRelaxedScannedExtension[]> {
|
||||
const stat = await this.fileService.resolve(input.location);
|
||||
if (stat.children) {
|
||||
let obsolete: IStringDictionary<boolean> = {};
|
||||
if (input.excludeObsolete && input.type === ExtensionType.User) {
|
||||
try {
|
||||
const raw = (await this.fileService.readFile(joinPath(input.location, '.obsolete'))).value.toString();
|
||||
obsolete = JSON.parse(raw);
|
||||
} catch (error) { /* ignore */ }
|
||||
}
|
||||
const extensions = await Promise.all<IRelaxedScannedExtension | null>(
|
||||
stat.children.map(async c => {
|
||||
if (!c.isDirectory) {
|
||||
return null;
|
||||
}
|
||||
// Do not consider user extension folder starting with `.`
|
||||
if (input.type === ExtensionType.User && basename(c.resource).indexOf('.') === 0) {
|
||||
return null;
|
||||
}
|
||||
const extensionScannerInput = new ExtensionScannerInput(c.resource, input.mtime, input.type, input.excludeObsolete, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations);
|
||||
const extension = await this.scanExtension(extensionScannerInput);
|
||||
return extension && !obsolete[ExtensionKey.create(extension).toString()] ? extension : null;
|
||||
}));
|
||||
return coalesce(extensions);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async scanOneOrMultipleExtensions(input: ExtensionScannerInput): Promise<IRelaxedScannedExtension[]> {
|
||||
try {
|
||||
if (await this.fileService.exists(joinPath(input.location, 'package.json'))) {
|
||||
const extension = await this.scanExtension(input);
|
||||
return extension ? [extension] : [];
|
||||
} else {
|
||||
return await this.scanExtensions(input);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(`Error scanning extensions at ${input.location.path}:`, getErrorMessage(error));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async scanExtension(input: ExtensionScannerInput): Promise<IRelaxedScannedExtension | null> {
|
||||
try {
|
||||
let manifest = await this.scanExtensionManifest(input.location);
|
||||
if (manifest) {
|
||||
// allow publisher to be undefined to make the initial extension authoring experience smoother
|
||||
if (!manifest.publisher) {
|
||||
manifest.publisher = UNDEFINED_PUBLISHER;
|
||||
}
|
||||
const metadata = manifest.__metadata;
|
||||
delete manifest.__metadata;
|
||||
const id = getGalleryExtensionId(manifest.publisher, manifest.name);
|
||||
const identifier = metadata?.id ? { id, uuid: metadata.id } : { id };
|
||||
const type = metadata?.isSystem ? ExtensionType.System : input.type;
|
||||
const isBuiltin = type === ExtensionType.System || !!metadata?.isBuiltin;
|
||||
manifest = await this.translateManifest(input.location, manifest, ExtensionScannerInput.createNlsConfiguration(input));
|
||||
const extension = {
|
||||
type,
|
||||
identifier,
|
||||
manifest,
|
||||
location: input.location,
|
||||
isBuiltin,
|
||||
targetPlatform: metadata?.targetPlatform ?? TargetPlatform.UNDEFINED,
|
||||
metadata,
|
||||
isValid: true,
|
||||
validations: []
|
||||
};
|
||||
return input.validate ? this.validate(extension, input) : extension;
|
||||
}
|
||||
} catch (e) {
|
||||
if (input.type !== ExtensionType.System) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
validate(extension: IRelaxedScannedExtension, input: ExtensionScannerInput): IRelaxedScannedExtension {
|
||||
let isValid = true;
|
||||
const validations = validateExtensionManifest(input.productVersion, input.productDate, input.location, extension.manifest, extension.isBuiltin);
|
||||
for (const [severity, message] of validations) {
|
||||
if (severity === Severity.Error) {
|
||||
isValid = false;
|
||||
this.logService.error(this.formatMessage(input.location, message));
|
||||
}
|
||||
}
|
||||
extension.isValid = isValid;
|
||||
extension.validations = validations;
|
||||
return extension;
|
||||
}
|
||||
|
||||
private async scanExtensionManifest(extensionLocation: URI): Promise<IScannedExtensionManifest | null> {
|
||||
const manifestLocation = joinPath(extensionLocation, 'package.json');
|
||||
let content;
|
||||
try {
|
||||
content = (await this.fileService.readFile(manifestLocation)).value.toString();
|
||||
} catch (error) {
|
||||
if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {
|
||||
this.logService.error(this.formatMessage(extensionLocation, localize('fileReadFail', "Cannot read file {0}: {1}.", manifestLocation.path, error.message)));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
let manifest: IScannedExtensionManifest;
|
||||
try {
|
||||
manifest = JSON.parse(content);
|
||||
} catch (err) {
|
||||
// invalid JSON, let's get good errors
|
||||
const errors: ParseError[] = [];
|
||||
parse(content, errors);
|
||||
for (const e of errors) {
|
||||
this.logService.error(this.formatMessage(extensionLocation, localize('jsonParseFail', "Failed to parse {0}: [{1}, {2}] {3}.", manifestLocation.path, e.offset, e.length, getParseErrorMessage(e.error))));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (getNodeType(manifest) !== 'object') {
|
||||
this.logService.error(this.formatMessage(extensionLocation, localize('jsonParseInvalidType', "Invalid manifest file {0}: Not an JSON object.", manifestLocation.path)));
|
||||
return null;
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
private async translateManifest(extensionLocation: URI, extensionManifest: IExtensionManifest, nlsConfiguration: NlsConfiguration): Promise<IExtensionManifest> {
|
||||
const localizedMessages = await this.getLocalizedMessages(extensionLocation, extensionManifest, nlsConfiguration);
|
||||
if (localizedMessages) {
|
||||
try {
|
||||
const errors: ParseError[] = [];
|
||||
// resolveOriginalMessageBundle returns null if localizedMessages.default === undefined;
|
||||
const defaults = await this.resolveOriginalMessageBundle(localizedMessages.default, errors);
|
||||
if (errors.length > 0) {
|
||||
errors.forEach((error) => {
|
||||
this.logService.error(this.formatMessage(extensionLocation, localize('jsonsParseReportErrors', "Failed to parse {0}: {1}.", localizedMessages.default?.path, getParseErrorMessage(error.error))));
|
||||
});
|
||||
return extensionManifest;
|
||||
} else if (getNodeType(localizedMessages) !== 'object') {
|
||||
this.logService.error(this.formatMessage(extensionLocation, localize('jsonInvalidFormat', "Invalid format {0}: JSON object expected.", localizedMessages.default?.path)));
|
||||
return extensionManifest;
|
||||
}
|
||||
const localized = localizedMessages.values || Object.create(null);
|
||||
this.replaceNLStrings(nlsConfiguration.pseudo, extensionManifest, localized, defaults, extensionLocation);
|
||||
} catch (error) {
|
||||
/*Ignore Error*/
|
||||
}
|
||||
}
|
||||
return extensionManifest;
|
||||
}
|
||||
|
||||
private async getLocalizedMessages(extensionLocation: URI, extensionManifest: IExtensionManifest, nlsConfiguration: NlsConfiguration): Promise<LocalizedMessages | undefined> {
|
||||
const defaultPackageNLS = joinPath(extensionLocation, 'package.nls.json');
|
||||
const reportErrors = (localized: URI | null, errors: ParseError[]): void => {
|
||||
errors.forEach((error) => {
|
||||
this.logService.error(this.formatMessage(extensionLocation, localize('jsonsParseReportErrors', "Failed to parse {0}: {1}.", localized?.path, getParseErrorMessage(error.error))));
|
||||
});
|
||||
};
|
||||
const reportInvalidFormat = (localized: URI | null): void => {
|
||||
this.logService.error(this.formatMessage(extensionLocation, localize('jsonInvalidFormat', "Invalid format {0}: JSON object expected.", localized?.path)));
|
||||
};
|
||||
|
||||
const translationId = `${extensionManifest.publisher}.${extensionManifest.name}`;
|
||||
const translationPath = nlsConfiguration.translations[translationId];
|
||||
|
||||
if (translationPath) {
|
||||
try {
|
||||
const translationResource = URI.file(translationPath);
|
||||
const content = (await this.fileService.readFile(translationResource)).value.toString();
|
||||
let errors: ParseError[] = [];
|
||||
let translationBundle: TranslationBundle = parse(content, errors);
|
||||
if (errors.length > 0) {
|
||||
reportErrors(translationResource, errors);
|
||||
return { values: undefined, default: defaultPackageNLS };
|
||||
} else if (getNodeType(translationBundle) !== 'object') {
|
||||
reportInvalidFormat(translationResource);
|
||||
return { values: undefined, default: defaultPackageNLS };
|
||||
} else {
|
||||
let values = translationBundle.contents ? translationBundle.contents.package : undefined;
|
||||
return { values: values, default: defaultPackageNLS };
|
||||
}
|
||||
} catch (error) {
|
||||
return { values: undefined, default: defaultPackageNLS };
|
||||
}
|
||||
} else {
|
||||
const exists = await this.fileService.exists(defaultPackageNLS);
|
||||
if (!exists) {
|
||||
return undefined;
|
||||
}
|
||||
let messageBundle;
|
||||
try {
|
||||
messageBundle = await this.findMessageBundles(extensionLocation, nlsConfiguration);
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
if (!messageBundle.localized) {
|
||||
return { values: undefined, default: messageBundle.original };
|
||||
}
|
||||
try {
|
||||
const messageBundleContent = (await this.fileService.readFile(messageBundle.localized)).value.toString();
|
||||
let errors: ParseError[] = [];
|
||||
let messages: MessageBag = parse(messageBundleContent, errors);
|
||||
if (errors.length > 0) {
|
||||
reportErrors(messageBundle.localized, errors);
|
||||
return { values: undefined, default: messageBundle.original };
|
||||
} else if (getNodeType(messages) !== 'object') {
|
||||
reportInvalidFormat(messageBundle.localized);
|
||||
return { values: undefined, default: messageBundle.original };
|
||||
}
|
||||
return { values: messages, default: messageBundle.original };
|
||||
} catch (error) {
|
||||
return { values: undefined, default: messageBundle.original };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses original message bundle, returns null if the original message bundle is null.
|
||||
*/
|
||||
private async resolveOriginalMessageBundle(originalMessageBundle: URI | null, errors: ParseError[]): Promise<{ [key: string]: string } | null> {
|
||||
if (originalMessageBundle) {
|
||||
try {
|
||||
const originalBundleContent = (await this.fileService.readFile(originalMessageBundle)).value.toString();
|
||||
return parse(originalBundleContent, errors);
|
||||
} catch (error) {
|
||||
/* Ignore Error */
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds localized message bundle and the original (unlocalized) one.
|
||||
* If the localized file is not present, returns null for the original and marks original as localized.
|
||||
*/
|
||||
private findMessageBundles(extensionLocation: URI, nlsConfiguration: NlsConfiguration): Promise<{ localized: URI; original: URI | null }> {
|
||||
return new Promise<{ localized: URI; original: URI | null }>((c, e) => {
|
||||
const loop = (locale: string): void => {
|
||||
let toCheck = joinPath(extensionLocation, `package.nls.${locale}.json`);
|
||||
this.fileService.exists(toCheck).then(exists => {
|
||||
if (exists) {
|
||||
c({ localized: toCheck, original: joinPath(extensionLocation, 'package.nls.json') });
|
||||
}
|
||||
let index = locale.lastIndexOf('-');
|
||||
if (index === -1) {
|
||||
c({ localized: joinPath(extensionLocation, 'package.nls.json'), original: null });
|
||||
} else {
|
||||
locale = locale.substring(0, index);
|
||||
loop(locale);
|
||||
}
|
||||
});
|
||||
};
|
||||
if (nlsConfiguration.devMode || nlsConfiguration.pseudo || !nlsConfiguration.language) {
|
||||
return c({ localized: joinPath(extensionLocation, 'package.nls.json'), original: null });
|
||||
}
|
||||
loop(nlsConfiguration.language);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This routine makes the following assumptions:
|
||||
* The root element is an object literal
|
||||
*/
|
||||
private replaceNLStrings<T extends object>(pseudo: boolean, literal: T, messages: MessageBag, originalMessages: MessageBag | null, extensionLocation: URI): void {
|
||||
const processEntry = (obj: any, key: string | number, command?: boolean) => {
|
||||
const value = obj[key];
|
||||
if (isString(value)) {
|
||||
const str = <string>value;
|
||||
const length = str.length;
|
||||
if (length > 1 && str[0] === '%' && str[length - 1] === '%') {
|
||||
const messageKey = str.substr(1, length - 2);
|
||||
let translated = messages[messageKey];
|
||||
// If the messages come from a language pack they might miss some keys
|
||||
// Fill them from the original messages.
|
||||
if (translated === undefined && originalMessages) {
|
||||
translated = originalMessages[messageKey];
|
||||
}
|
||||
let message: string | undefined = typeof translated === 'string' ? translated : (typeof translated?.message === 'string' ? translated.message : undefined);
|
||||
if (message !== undefined) {
|
||||
if (pseudo) {
|
||||
// FF3B and FF3D is the Unicode zenkaku representation for [ and ]
|
||||
message = '\uFF3B' + message.replace(/[aouei]/g, '$&$&') + '\uFF3D';
|
||||
}
|
||||
obj[key] = command && (key === 'title' || key === 'category') && originalMessages ? { value: message, original: originalMessages[messageKey] } : message;
|
||||
} else {
|
||||
this.logService.warn(this.formatMessage(extensionLocation, localize('missingNLSKey', "Couldn't find message for key {0}.", messageKey)));
|
||||
}
|
||||
}
|
||||
} else if (isObject(value)) {
|
||||
for (let k in value) {
|
||||
if (value.hasOwnProperty(k)) {
|
||||
k === 'commands' ? processEntry(value, k, true) : processEntry(value, k, command);
|
||||
}
|
||||
}
|
||||
} else if (isArray(value)) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
processEntry(value, i, command);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (let key in literal) {
|
||||
if (literal.hasOwnProperty(key)) {
|
||||
processEntry(literal, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private formatMessage(extensionLocation: URI, message: string): string {
|
||||
return `[${extensionLocation.path}]: ${message}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface IExtensionCacheData {
|
||||
input: ExtensionScannerInput;
|
||||
result: IRelaxedScannedExtension[];
|
||||
}
|
||||
|
||||
class CachedExtensionsScanner extends ExtensionsScanner {
|
||||
|
||||
private input: ExtensionScannerInput | undefined;
|
||||
private readonly cacheValidatorThrottler: ThrottledDelayer<void> = this._register(new ThrottledDelayer(3000));
|
||||
|
||||
private readonly _onDidChangeCache = this._register(new Emitter<void>());
|
||||
readonly onDidChangeCache = this._onDidChangeCache.event;
|
||||
|
||||
constructor(
|
||||
private readonly cacheFile: URI,
|
||||
fileService: IFileService,
|
||||
logService: ILogService
|
||||
) {
|
||||
super(fileService, logService);
|
||||
}
|
||||
|
||||
override async scanExtensions(input: ExtensionScannerInput): Promise<IRelaxedScannedExtension[]> {
|
||||
const cacheContents = await this.readExtensionCache();
|
||||
this.input = input;
|
||||
if (cacheContents && cacheContents.input && ExtensionScannerInput.equals(cacheContents.input, this.input)) {
|
||||
this.cacheValidatorThrottler.trigger(() => this.validateCache());
|
||||
return cacheContents.result.map((extension) => {
|
||||
// revive URI object
|
||||
extension.location = URI.revive(extension.location);
|
||||
return extension;
|
||||
});
|
||||
}
|
||||
const result = await super.scanExtensions(input);
|
||||
await this.writeExtensionCache({ input, result });
|
||||
return result;
|
||||
}
|
||||
|
||||
private async readExtensionCache(): Promise<IExtensionCacheData | null> {
|
||||
try {
|
||||
const cacheRawContents = await this.fileService.readFile(this.cacheFile);
|
||||
const extensionCacheData: IExtensionCacheData = JSON.parse(cacheRawContents.value.toString());
|
||||
return { result: extensionCacheData.result, input: revive(extensionCacheData.input) };
|
||||
} catch (error) {
|
||||
this.logService.debug('Error while reading the extension cache file:', this.cacheFile.path, getErrorMessage(error));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async writeExtensionCache(cacheContents: IExtensionCacheData): Promise<void> {
|
||||
try {
|
||||
await this.fileService.writeFile(this.cacheFile, VSBuffer.fromString(JSON.stringify(cacheContents)));
|
||||
} catch (error) {
|
||||
this.logService.debug('Error while writing the extension cache file:', this.cacheFile.path, getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
private async validateCache(): Promise<void> {
|
||||
if (!this.input) {
|
||||
// Input has been unset by the time we get here, so skip validation
|
||||
return;
|
||||
}
|
||||
|
||||
const cacheContents = await this.readExtensionCache();
|
||||
if (!cacheContents) {
|
||||
// Cache has been deleted by someone else, which is perfectly fine...
|
||||
return;
|
||||
}
|
||||
|
||||
const actual = cacheContents.result;
|
||||
const expected = JSON.parse(JSON.stringify(await super.scanExtensions(this.input)));
|
||||
if (objects.equals(expected, actual)) {
|
||||
// Cache is valid and running with it is perfectly fine...
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Cache is invalid, delete it
|
||||
await this.fileService.del(this.cacheFile);
|
||||
this._onDidChangeCache.fire();
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function toExtensionDescription(extension: IScannedExtension, isUnderDevelopment: boolean): IExtensionDescription {
|
||||
const id = getExtensionId(extension.manifest.publisher, extension.manifest.name);
|
||||
return {
|
||||
id,
|
||||
identifier: new ExtensionIdentifier(id),
|
||||
isBuiltin: extension.type === ExtensionType.System,
|
||||
isUserBuiltin: extension.type === ExtensionType.User && extension.isBuiltin,
|
||||
isUnderDevelopment,
|
||||
extensionLocation: extension.location,
|
||||
uuid: extension.identifier.uuid,
|
||||
targetPlatform: extension.targetPlatform,
|
||||
...extension.manifest,
|
||||
};
|
||||
}
|
||||
|
||||
export class NativeExtensionsScannerService extends AbstractExtensionsScannerService implements IExtensionsScannerService {
|
||||
|
||||
private readonly translationsPromise: Promise<Translations>;
|
||||
|
||||
constructor(
|
||||
systemExtensionsLocation: URI,
|
||||
userExtensionsLocation: URI,
|
||||
userHome: URI,
|
||||
userDataPath: URI,
|
||||
fileService: IFileService,
|
||||
logService: ILogService,
|
||||
environmentService: IEnvironmentService,
|
||||
productService: IProductService,
|
||||
) {
|
||||
super(
|
||||
systemExtensionsLocation,
|
||||
userExtensionsLocation,
|
||||
joinPath(userHome, '.vscode-oss-dev', 'extensions', 'control.json'),
|
||||
joinPath(userDataPath, MANIFEST_CACHE_FOLDER),
|
||||
fileService, logService, environmentService, productService);
|
||||
this.translationsPromise = (async () => {
|
||||
if (platform.translationsConfigFile) {
|
||||
try {
|
||||
const content = await this.fileService.readFile(URI.file(platform.translationsConfigFile));
|
||||
return JSON.parse(content.value.toString());
|
||||
} catch (err) { /* Ignore Error */ }
|
||||
}
|
||||
return Object.create(null);
|
||||
})();
|
||||
}
|
||||
|
||||
protected getTranslations(language: string): Promise<Translations> {
|
||||
return this.translationsPromise;
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,68 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { areSameExtensions, getExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage';
|
||||
import { ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
/**
|
||||
* Migrates the installed unsupported nightly extension to a supported pre-release extension. It includes following:
|
||||
* - Uninstall the Unsupported extension
|
||||
* - Install (with optional storage migration) the Pre-release extension only if
|
||||
* - the extension is not installed
|
||||
* - or it is a release version and the unsupported extension is enabled.
|
||||
*/
|
||||
export async function migrateUnsupportedExtensions(extensionManagementService: IExtensionManagementService, galleryService: IExtensionGalleryService, extensionStorageService: IExtensionStorageService, extensionEnablementService: IGlobalExtensionEnablementService, logService: ILogService): Promise<void> {
|
||||
try {
|
||||
const extensionsControlManifest = await extensionManagementService.getExtensionsControlManifest();
|
||||
if (!extensionsControlManifest.unsupportedPreReleaseExtensions) {
|
||||
return;
|
||||
}
|
||||
const installed = await extensionManagementService.getInstalled(ExtensionType.User);
|
||||
for (const [unsupportedExtensionId, { id: preReleaseExtensionId, migrateStorage }] of Object.entries(extensionsControlManifest.unsupportedPreReleaseExtensions)) {
|
||||
const unsupportedExtension = installed.find(i => areSameExtensions(i.identifier, { id: unsupportedExtensionId }));
|
||||
// Unsupported Extension is not installed
|
||||
if (!unsupportedExtension) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const gallery = (await galleryService.getExtensions([{ id: preReleaseExtensionId, preRelease: true }], { targetPlatform: await extensionManagementService.getTargetPlatform(), compatible: true }, CancellationToken.None))[0];
|
||||
if (!gallery) {
|
||||
logService.info(`Skipping migrating '${unsupportedExtension.identifier.id}' extension because, the comaptible target '${preReleaseExtensionId}' extension is not found`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
logService.info(`Migrating '${unsupportedExtension.identifier.id}' extension to '${preReleaseExtensionId}' extension...`);
|
||||
|
||||
const isUnsupportedExtensionEnabled = !extensionEnablementService.getDisabledExtensions().some(e => areSameExtensions(e, unsupportedExtension.identifier));
|
||||
await extensionManagementService.uninstall(unsupportedExtension);
|
||||
logService.info(`Uninstalled the unsupported extension '${unsupportedExtension.identifier.id}'`);
|
||||
|
||||
let preReleaseExtension = installed.find(i => areSameExtensions(i.identifier, { id: preReleaseExtensionId }));
|
||||
if (!preReleaseExtension || (!preReleaseExtension.isPreReleaseVersion && isUnsupportedExtensionEnabled)) {
|
||||
preReleaseExtension = await extensionManagementService.installFromGallery(gallery, { installPreReleaseVersion: true, isMachineScoped: unsupportedExtension.isMachineScoped, operation: InstallOperation.Migrate });
|
||||
logService.info(`Installed the pre-release extension '${preReleaseExtension.identifier.id}'`);
|
||||
if (!isUnsupportedExtensionEnabled) {
|
||||
await extensionEnablementService.disableExtension(preReleaseExtension.identifier);
|
||||
logService.info(`Disabled the pre-release extension '${preReleaseExtension.identifier.id}' because the unsupported extension '${unsupportedExtension.identifier.id}' is disabled`);
|
||||
}
|
||||
if (migrateStorage) {
|
||||
extensionStorageService.addToMigrationList(getExtensionId(unsupportedExtension.manifest.publisher, unsupportedExtension.manifest.name), getExtensionId(preReleaseExtension.manifest.publisher, preReleaseExtension.manifest.name));
|
||||
logService.info(`Added pre-release extension to the storage migration list`);
|
||||
}
|
||||
}
|
||||
logService.info(`Migrated '${unsupportedExtension.identifier.id}' extension to '${preReleaseExtensionId}' extension.`);
|
||||
} catch (error) {
|
||||
logService.error(error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logService.error(error);
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,10 @@ import { URI } from 'vs/base/common/uri';
|
||||
import { localize } from 'vs/nls';
|
||||
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IExecutableBasedExtensionTip, IExtensionManagementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { ExtensionTipsService as BaseExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionTipsService';
|
||||
import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
|
||||
import { ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
|
||||
@@ -25,14 +27,14 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
type ExeExtensionRecommendationsClassification = {
|
||||
extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
|
||||
exeName: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
|
||||
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 }[];
|
||||
readonly exeFriendlyName: string;
|
||||
readonly windowsPath?: string;
|
||||
readonly recommendations: { extensionId: string; extensionName: string; isExtensionPack: boolean; whenNotInstalled?: string[] }[];
|
||||
};
|
||||
|
||||
const promptedExecutableTipsStorageKey = 'extensionTips/promptedExecutableTips';
|
||||
@@ -64,9 +66,9 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
|
||||
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 }[] = [];
|
||||
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) {
|
||||
@@ -130,13 +132,13 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
|
||||
for (const extensionId of installed) {
|
||||
const tip = importantExeBasedRecommendations.get(extensionId);
|
||||
if (tip) {
|
||||
this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:alreadyInstalled', { extensionId, exeName: tip.exeName });
|
||||
this.telemetryService.publicLog2<{ exeName: string; extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:alreadyInstalled', { extensionId, exeName: tip.exeName });
|
||||
}
|
||||
}
|
||||
for (const extensionId of recommendations) {
|
||||
const tip = importantExeBasedRecommendations.get(extensionId);
|
||||
if (tip) {
|
||||
this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:notInstalled', { extensionId, exeName: tip.exeName });
|
||||
this.telemetryService.publicLog2<{ exeName: string; extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:notInstalled', { extensionId, exeName: tip.exeName });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,15 +177,17 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
|
||||
case RecommendationsNotificationResult.Ignored:
|
||||
this.highImportanceTipsByExe.delete(exeName);
|
||||
break;
|
||||
case RecommendationsNotificationResult.IncompatibleWindow:
|
||||
case RecommendationsNotificationResult.IncompatibleWindow: {
|
||||
// Recommended in incompatible window. Schedule the prompt after active window change
|
||||
const onActiveWindowChange = Event.once(Event.latch(Event.any(this.nativeHostService.onDidOpenWindow, this.nativeHostService.onDidFocusWindow)));
|
||||
this._register(onActiveWindowChange(() => this.promptHighImportanceExeBasedTip()));
|
||||
break;
|
||||
case RecommendationsNotificationResult.TooMany:
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -209,7 +213,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
|
||||
this.promptExeRecommendations(tips)
|
||||
.then(result => {
|
||||
switch (result) {
|
||||
case RecommendationsNotificationResult.Accepted:
|
||||
case RecommendationsNotificationResult.Accepted: {
|
||||
// Accepted: Update the last prompted time and caches.
|
||||
this.updateLastPromptedMediumExeTime(Date.now());
|
||||
this.mediumImportanceTipsByExe.delete(exeName);
|
||||
@@ -218,29 +222,33 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
|
||||
// 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.IncompatibleWindow:
|
||||
case RecommendationsNotificationResult.IncompatibleWindow: {
|
||||
// Recommended in incompatible window. Schedule the prompt after active window change
|
||||
const onActiveWindowChange = Event.once(Event.latch(Event.any(this.nativeHostService.onDidOpenWindow, this.nativeHostService.onDidFocusWindow)));
|
||||
this._register(onActiveWindowChange(() => this.promptMediumImportanceExeBasedTip()));
|
||||
break;
|
||||
|
||||
case RecommendationsNotificationResult.TooMany:
|
||||
}
|
||||
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());
|
||||
private async promptExeRecommendations(tips: IExecutableBasedExtensionTip[]): Promise<RecommendationsNotificationResult> {
|
||||
const installed = await this.extensionManagementService.getInstalled(ExtensionType.User);
|
||||
const extensionIds = tips
|
||||
.filter(tip => !tip.whenNotInstalled || tip.whenNotInstalled.every(id => installed.every(local => !areSameExtensions(local.identifier, { id }))))
|
||||
.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);
|
||||
}
|
||||
@@ -268,7 +276,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
|
||||
this.storageService.store(promptedExecutableTipsStorageKey, JSON.stringify(promptedExecutableTips), StorageScope.GLOBAL, StorageTarget.USER);
|
||||
}
|
||||
|
||||
private groupByInstalled(recommendationsToSuggest: string[], local: ILocalExtension[]): { installed: string[], uninstalled: string[] } {
|
||||
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 => {
|
||||
@@ -313,7 +321,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
|
||||
checkedExecutables.set(exePath, exists);
|
||||
}
|
||||
if (exists) {
|
||||
for (const { extensionId, extensionName, isExtensionPack } of extensionTip.recommendations) {
|
||||
for (const { extensionId, extensionName, isExtensionPack, whenNotInstalled } of extensionTip.recommendations) {
|
||||
result.push({
|
||||
extensionId,
|
||||
extensionName,
|
||||
@@ -321,6 +329,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
|
||||
exeName,
|
||||
exeFriendlyName: extensionTip.exeFriendlyName,
|
||||
windowsPath: extensionTip.windowsPath,
|
||||
whenNotInstalled: whenNotInstalled
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IExtensionsScannerService, NativeExtensionsScannerService, } from 'vs/platform/extensionManagement/common/extensionsScannerService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
|
||||
export class ExtensionsScannerService extends NativeExtensionsScannerService implements IExtensionsScannerService {
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@ILogService logService: ILogService,
|
||||
@INativeEnvironmentService environmentService: INativeEnvironmentService,
|
||||
@IProductService productService: IProductService,
|
||||
) {
|
||||
super(
|
||||
URI.file(environmentService.builtinExtensionsPath),
|
||||
URI.file(environmentService.extensionsPath),
|
||||
environmentService.userHome,
|
||||
URI.file(environmentService.userDataPath),
|
||||
fileService, logService, environmentService, productService);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IExtensionsScannerService, ExtensionsScannerService);
|
||||
@@ -13,13 +13,11 @@ import { URI } from 'vs/base/common/uri';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { Promises as FSPromises } from 'vs/base/node/pfs';
|
||||
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IExtensionGalleryService, IGalleryExtension, InstallOperation, TargetPlatform } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { ExtensionIdentifierWithVersion, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { ExtensionKey, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
const ExtensionIdVersionRegex = /^([^.]+\..+)-(\d+\.\d+\.\d+)$/;
|
||||
|
||||
export class ExtensionsDownloader extends Disposable {
|
||||
|
||||
private readonly extensionsDownloadDir: URI;
|
||||
@@ -97,9 +95,9 @@ export class ExtensionsDownloader extends Disposable {
|
||||
const folderStat = await this.fileService.resolve(this.extensionsDownloadDir, { resolveMetadata: true });
|
||||
if (folderStat.children) {
|
||||
const toDelete: URI[] = [];
|
||||
const all: [ExtensionIdentifierWithVersion, IFileStatWithMetadata][] = [];
|
||||
const all: [ExtensionKey, IFileStatWithMetadata][] = [];
|
||||
for (const stat of folderStat.children) {
|
||||
const extension = this.parse(stat.name);
|
||||
const extension = ExtensionKey.parse(stat.name);
|
||||
if (extension) {
|
||||
all.push([extension, stat]);
|
||||
}
|
||||
@@ -124,11 +122,7 @@ export class ExtensionsDownloader extends Disposable {
|
||||
}
|
||||
|
||||
private getName(extension: IGalleryExtension): string {
|
||||
return this.cache ? `${new ExtensionIdentifierWithVersion(extension.identifier, extension.version).key().toLowerCase()}${extension.properties.targetPlatform !== TargetPlatform.UNDEFINED ? `-${extension.properties.targetPlatform}` : ''}` : generateUuid();
|
||||
return this.cache ? ExtensionKey.create(extension).toString().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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export class ExtensionsLifecycle extends Disposable {
|
||||
return Promises.rm(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 {
|
||||
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(' ');
|
||||
@@ -97,7 +97,7 @@ export class ExtensionsLifecycle extends Disposable {
|
||||
const extensionUninstallProcess = fork(uninstallHook, [`--type=extension-post-${lifecycleType}`, ...args], opts);
|
||||
|
||||
// Catch all output coming from the process
|
||||
type Output = { data: string, format: string[] };
|
||||
type Output = { data: string; format: string[] };
|
||||
extensionUninstallProcess.stdout!.setEncoding('utf8');
|
||||
extensionUninstallProcess.stderr!.setEncoding('utf8');
|
||||
|
||||
|
||||
@@ -4,46 +4,50 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { extensionsWorkbenchServiceIncompatible } from 'sql/base/common/locConstants';
|
||||
import { Promises, Queue } from 'vs/base/common/async';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import { isLinux, isMacintosh, platform } from 'vs/base/common/platform';
|
||||
import { arch } from 'vs/base/common/process';
|
||||
import { isMacintosh, isWindows } from 'vs/base/common/platform';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import * as semver from 'vs/base/common/semver/semver';
|
||||
import { isBoolean, isUndefined } from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { IFile, zip } from 'vs/base/node/zip';
|
||||
import { extract, ExtractError, IFile, zip } from 'vs/base/node/zip';
|
||||
import * as nls from 'vs/nls';
|
||||
import { IDownloadService } from 'vs/platform/download/common/download';
|
||||
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, IUninstallExtensionTask, joinErrors, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService';
|
||||
import {
|
||||
ExtensionManagementError, ExtensionManagementErrorCode, getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallOperation, InstallOptions,
|
||||
InstallVSIXOptions, TargetPlatform
|
||||
ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallOperation, InstallOptions,
|
||||
InstallVSIXOptions, Metadata
|
||||
} from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { areSameExtensions, ExtensionIdentifierWithVersion, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IExtensionsScannerService, IScannedExtension, ScanOptions } from 'vs/platform/extensionManagement/common/extensionsScannerService';
|
||||
import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader';
|
||||
import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle';
|
||||
import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil';
|
||||
import { ExtensionsManifestCache } from 'vs/platform/extensionManagement/node/extensionsManifestCache';
|
||||
import { ExtensionsScanner, ILocalExtensionManifest, IMetadata } from 'vs/platform/extensionManagement/node/extensionsScanner';
|
||||
import { ExtensionsWatcher } from 'vs/platform/extensionManagement/node/extensionsWatcher';
|
||||
import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
|
||||
import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions';
|
||||
import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
|
||||
|
||||
interface InstallableExtension {
|
||||
zipPath: string;
|
||||
identifierWithVersion: ExtensionIdentifierWithVersion;
|
||||
metadata?: IMetadata;
|
||||
key: ExtensionKey;
|
||||
metadata?: Metadata;
|
||||
}
|
||||
|
||||
export class ExtensionManagementService extends AbstractExtensionManagementService implements IExtensionManagementService {
|
||||
@@ -60,14 +64,15 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
|
||||
@IDownloadService private downloadService: IDownloadService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IProductService productService: IProductService
|
||||
@IProductService productService: IProductService,
|
||||
@IUriIdentityService uriIdentityService: IUriIdentityService
|
||||
) {
|
||||
super(galleryService, telemetryService, logService, productService);
|
||||
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));
|
||||
const extensionsWatcher = this._register(new ExtensionsWatcher(this, fileService, environmentService, logService));
|
||||
const extensionsWatcher = this._register(new ExtensionsWatcher(this, fileService, environmentService, logService, uriIdentityService));
|
||||
|
||||
this._register(extensionsWatcher.onDidChangeExtensionsByAnotherSource(({ added, removed }) => {
|
||||
if (added.length) {
|
||||
@@ -80,36 +85,11 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
|
||||
private _targetPlatformPromise: Promise<TargetPlatform> | undefined;
|
||||
getTargetPlatform(): Promise<TargetPlatform> {
|
||||
if (!this._targetPlatformPromise) {
|
||||
this._targetPlatformPromise = (async () => {
|
||||
const isAlpineLinux = await this.isAlpineLinux();
|
||||
const targetPlatform = getTargetPlatform(isAlpineLinux ? 'alpine' : platform, arch);
|
||||
this.logService.debug('ExtensionManagementService#TargetPlatform:', targetPlatform);
|
||||
return targetPlatform;
|
||||
})();
|
||||
this._targetPlatformPromise = computeTargetPlatform(this.fileService, this.logService);
|
||||
}
|
||||
return this._targetPlatformPromise;
|
||||
}
|
||||
|
||||
private async isAlpineLinux(): Promise<boolean> {
|
||||
if (!isLinux) {
|
||||
return false;
|
||||
}
|
||||
let content: string | undefined;
|
||||
try {
|
||||
const fileContent = await this.fileService.readFile(URI.file('/etc/os-release'));
|
||||
content = fileContent.value.toString();
|
||||
} catch (error) {
|
||||
try {
|
||||
const fileContent = await this.fileService.readFile(URI.file('/usr/lib/os-release'));
|
||||
content = fileContent.value.toString();
|
||||
} catch (error) {
|
||||
/* Ignore */
|
||||
this.logService.debug(`Error while getting the os-release file.`, getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
return !!content && (content.match(/^ID=([^\u001b\r\n]*)/m) || [])[1] === 'alpine';
|
||||
}
|
||||
|
||||
async zip(extension: ILocalExtension): Promise<URI> {
|
||||
this.logService.trace('ExtensionManagementService#zip', extension.identifier.id);
|
||||
const files = await this.collectFiles(extension);
|
||||
@@ -157,14 +137,18 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
|
||||
|
||||
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 });
|
||||
const localMetadata: Metadata = { ...metadata };
|
||||
if (metadata.isPreReleaseVersion) {
|
||||
localMetadata.preRelease = true;
|
||||
}
|
||||
local = await this.extensionsScanner.updateMetadata(local, localMetadata);
|
||||
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 });
|
||||
local = await this.extensionsScanner.updateMetadata(local, { isMachineScoped });
|
||||
this.manifestCache.invalidate();
|
||||
return local;
|
||||
}
|
||||
@@ -217,14 +201,281 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
|
||||
|
||||
}
|
||||
|
||||
class ExtensionsScanner extends Disposable {
|
||||
|
||||
private readonly uninstalledPath: string;
|
||||
private readonly uninstalledFileLimiter: Queue<any>;
|
||||
|
||||
constructor(
|
||||
private readonly beforeRemovingExtension: (e: ILocalExtension) => Promise<void>,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
) {
|
||||
super();
|
||||
this.uninstalledPath = joinPath(this.extensionsScannerService.userExtensionsLocation, '.obsolete').fsPath;
|
||||
this.uninstalledFileLimiter = new Queue();
|
||||
}
|
||||
|
||||
async cleanUp(): Promise<void> {
|
||||
await this.removeUninstalledExtensions();
|
||||
await this.removeOutdatedExtensions();
|
||||
}
|
||||
|
||||
async scanExtensions(type: ExtensionType | null): Promise<ILocalExtension[]> {
|
||||
const scannedOptions: ScanOptions = { includeInvalid: true };
|
||||
let scannedExtensions: IScannedExtension[] = [];
|
||||
if (type === null || type === ExtensionType.System) {
|
||||
scannedExtensions.push(...await this.extensionsScannerService.scanAllExtensions(scannedOptions));
|
||||
} else if (type === ExtensionType.User) {
|
||||
scannedExtensions.push(...await this.extensionsScannerService.scanUserExtensions(scannedOptions));
|
||||
}
|
||||
scannedExtensions = type !== null ? scannedExtensions.filter(r => r.type === type) : scannedExtensions;
|
||||
return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension)));
|
||||
}
|
||||
|
||||
async scanUserExtensions(excludeOutdated: boolean): Promise<ILocalExtension[]> {
|
||||
const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: !excludeOutdated, includeInvalid: true });
|
||||
return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension)));
|
||||
}
|
||||
|
||||
async extractUserExtension(extensionKey: ExtensionKey, zipPath: string, metadata: Metadata | undefined, token: CancellationToken): Promise<ILocalExtension> {
|
||||
const folderName = extensionKey.toString();
|
||||
const tempPath = path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, `.${generateUuid()}`);
|
||||
const extensionPath = path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, folderName);
|
||||
|
||||
try {
|
||||
await pfs.Promises.rm(extensionPath);
|
||||
} catch (error) {
|
||||
throw new ExtensionManagementError(nls.localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionPath, extensionKey.id), ExtensionManagementErrorCode.Delete);
|
||||
}
|
||||
|
||||
await this.extractAtLocation(extensionKey, zipPath, tempPath, token);
|
||||
await this.extensionsScannerService.updateMetadata(URI.file(tempPath), { ...metadata, installedTimestamp: Date.now() });
|
||||
|
||||
try {
|
||||
await this.rename(extensionKey, tempPath, extensionPath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */);
|
||||
this.logService.info('Renamed to', extensionPath);
|
||||
} catch (error) {
|
||||
try {
|
||||
await pfs.Promises.rm(tempPath);
|
||||
} catch (e) { /* ignore */ }
|
||||
if (error.code === 'ENOTEMPTY') {
|
||||
this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, extensionKey.id);
|
||||
} else {
|
||||
this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted from extracted location`, tempPath);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return this.scanLocalExtension(URI.file(extensionPath), ExtensionType.User);
|
||||
}
|
||||
|
||||
async updateMetadata(local: ILocalExtension, metadata: Partial<Metadata>): Promise<ILocalExtension> {
|
||||
await this.extensionsScannerService.updateMetadata(local.location, metadata);
|
||||
return this.scanLocalExtension(local.location, local.type);
|
||||
}
|
||||
|
||||
getUninstalledExtensions(): Promise<IStringDictionary<boolean>> {
|
||||
return this.withUninstalledExtensions();
|
||||
}
|
||||
|
||||
async setUninstalled(...extensions: ILocalExtension[]): Promise<void> {
|
||||
const extensionKeys: ExtensionKey[] = extensions.map(e => ExtensionKey.create(e));
|
||||
await this.withUninstalledExtensions(uninstalled => {
|
||||
extensionKeys.forEach(extensionKey => uninstalled[extensionKey.toString()] = true);
|
||||
});
|
||||
}
|
||||
|
||||
async setInstalled(extensionKey: ExtensionKey): Promise<ILocalExtension | null> {
|
||||
await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]);
|
||||
const userExtensions = await this.scanUserExtensions(true);
|
||||
const localExtension = userExtensions.find(i => ExtensionKey.create(i).equals(extensionKey)) || null;
|
||||
if (!localExtension) {
|
||||
return null;
|
||||
}
|
||||
return this.updateMetadata(localExtension, { installedTimestamp: Date.now() });
|
||||
}
|
||||
|
||||
async removeExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise<void> {
|
||||
this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id, extension.location.fsPath);
|
||||
await pfs.Promises.rm(extension.location.fsPath);
|
||||
this.logService.info('Deleted from disk', extension.identifier.id, extension.location.fsPath);
|
||||
}
|
||||
|
||||
async removeUninstalledExtension(extension: ILocalExtension | IScannedExtension): Promise<void> {
|
||||
await this.removeExtension(extension, 'uninstalled');
|
||||
await this.withUninstalledExtensions(uninstalled => delete uninstalled[ExtensionKey.create(extension).toString()]);
|
||||
}
|
||||
|
||||
private async withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary<boolean>) => void): Promise<IStringDictionary<boolean>> {
|
||||
return this.uninstalledFileLimiter.queue(async () => {
|
||||
let raw: string | undefined;
|
||||
try {
|
||||
raw = await pfs.Promises.readFile(this.uninstalledPath, 'utf8');
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
let uninstalled = {};
|
||||
if (raw) {
|
||||
try {
|
||||
uninstalled = JSON.parse(raw);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
if (updateFn) {
|
||||
updateFn(uninstalled);
|
||||
if (Object.keys(uninstalled).length) {
|
||||
await pfs.Promises.writeFile(this.uninstalledPath, JSON.stringify(uninstalled));
|
||||
} else {
|
||||
await pfs.Promises.rm(this.uninstalledPath);
|
||||
}
|
||||
}
|
||||
|
||||
return uninstalled;
|
||||
});
|
||||
}
|
||||
|
||||
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.Promises.rm(location);
|
||||
} catch (e) {
|
||||
throw new ExtensionManagementError(this.joinErrors(e).message, ExtensionManagementErrorCode.Delete);
|
||||
}
|
||||
|
||||
try {
|
||||
await extract(zipPath, location, { sourcePath: 'extension', overwrite: true }, token);
|
||||
this.logService.info(`Extracted extension to ${location}:`, identifier.id);
|
||||
} catch (e) {
|
||||
try { await pfs.Promises.rm(location); } catch (e) { /* Ignore */ }
|
||||
let errorCode = ExtensionManagementErrorCode.Extract;
|
||||
if (e instanceof ExtractError) {
|
||||
if (e.type === 'CorruptZip') {
|
||||
errorCode = ExtensionManagementErrorCode.CorruptZip;
|
||||
} else if (e.type === 'Incomplete') {
|
||||
errorCode = ExtensionManagementErrorCode.IncompleteZip;
|
||||
}
|
||||
}
|
||||
throw new ExtensionManagementError(e.message, errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
private async rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise<void> {
|
||||
try {
|
||||
await pfs.Promises.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 || nls.localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || ExtensionManagementErrorCode.Rename);
|
||||
}
|
||||
}
|
||||
|
||||
private async scanLocalExtension(location: URI, type: ExtensionType): Promise<ILocalExtension> {
|
||||
const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true });
|
||||
if (scannedExtension) {
|
||||
return this.toLocalExtension(scannedExtension);
|
||||
}
|
||||
throw new Error(nls.localize('cannot read', "Cannot read the extension from {0}", location.path));
|
||||
}
|
||||
|
||||
private async toLocalExtension(extension: IScannedExtension): Promise<ILocalExtension> {
|
||||
const stat = await this.fileService.resolve(extension.location);
|
||||
let readmeUrl: URI | undefined;
|
||||
let changelogUrl: URI | undefined;
|
||||
if (stat.children) {
|
||||
readmeUrl = stat.children.find(({ name }) => /^readme(\.txt|\.md|)$/i.test(name))?.resource;
|
||||
changelogUrl = stat.children.find(({ name }) => /^changelog(\.txt|\.md|)$/i.test(name))?.resource;
|
||||
}
|
||||
return {
|
||||
identifier: extension.identifier,
|
||||
type: extension.type,
|
||||
isBuiltin: extension.isBuiltin || !!extension.metadata?.isBuiltin,
|
||||
location: extension.location,
|
||||
manifest: extension.manifest,
|
||||
targetPlatform: extension.targetPlatform,
|
||||
validations: extension.validations,
|
||||
isValid: extension.isValid,
|
||||
readmeUrl,
|
||||
changelogUrl,
|
||||
publisherDisplayName: extension.metadata?.publisherDisplayName || null,
|
||||
publisherId: extension.metadata?.publisherId || null,
|
||||
isMachineScoped: !!extension.metadata?.isMachineScoped,
|
||||
isPreReleaseVersion: !!extension.metadata?.isPreReleaseVersion,
|
||||
preRelease: !!extension.metadata?.preRelease,
|
||||
installedTimestamp: extension.metadata?.installedTimestamp,
|
||||
updated: !!extension.metadata?.updated,
|
||||
};
|
||||
}
|
||||
private async removeUninstalledExtensions(): Promise<void> {
|
||||
const uninstalled = await this.getUninstalledExtensions();
|
||||
const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeUninstalled: true, includeInvalid: true }); // All user extensions
|
||||
const installed: Set<string> = new Set<string>();
|
||||
for (const e of extensions) {
|
||||
if (!uninstalled[ExtensionKey.create(e).toString()]) {
|
||||
installed.add(e.identifier.id.toLowerCase());
|
||||
}
|
||||
}
|
||||
const byExtension = groupByExtension(extensions, e => e.identifier);
|
||||
await Promises.settled(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(await this.toLocalExtension(latest));
|
||||
}
|
||||
}));
|
||||
const toRemove = extensions.filter(e => uninstalled[ExtensionKey.create(e).toString()]);
|
||||
await Promises.settled(toRemove.map(e => this.removeUninstalledExtension(e)));
|
||||
}
|
||||
|
||||
private async removeOutdatedExtensions(): Promise<void> {
|
||||
const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeUninstalled: true, includeInvalid: true }); // All user extensions
|
||||
const toRemove: IScannedExtension[] = [];
|
||||
|
||||
// Outdated extensions
|
||||
const targetPlatform = await this.extensionsScannerService.getTargetPlatform();
|
||||
const byExtension = groupByExtension(extensions, e => e.identifier);
|
||||
toRemove.push(...byExtension.map(p => p.sort((a, b) => {
|
||||
const vcompare = semver.rcompare(a.manifest.version, b.manifest.version);
|
||||
if (vcompare !== 0) {
|
||||
return vcompare;
|
||||
}
|
||||
if (a.targetPlatform === targetPlatform) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}).slice(1)).flat());
|
||||
|
||||
await Promises.settled(toRemove.map(extension => this.removeExtension(extension, 'outdated')));
|
||||
}
|
||||
|
||||
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(''));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class AbstractInstallExtensionTask extends AbstractExtensionTask<ILocalExtension> implements IInstallExtensionTask {
|
||||
|
||||
protected _operation = InstallOperation.Install;
|
||||
get operation() { return this._operation; }
|
||||
get operation() { return isUndefined(this.options.operation) ? this._operation : this.options.operation; }
|
||||
|
||||
constructor(
|
||||
readonly identifier: IExtensionIdentifier,
|
||||
readonly source: URI | IGalleryExtension,
|
||||
protected readonly options: InstallOptions,
|
||||
protected readonly extensionsScanner: ExtensionsScanner,
|
||||
protected readonly logService: ILogService,
|
||||
) {
|
||||
@@ -233,9 +484,9 @@ abstract class AbstractInstallExtensionTask extends AbstractExtensionTask<ILocal
|
||||
|
||||
protected async installExtension(installableExtension: InstallableExtension, token: CancellationToken): Promise<ILocalExtension> {
|
||||
try {
|
||||
const local = await this.unsetUninstalledAndGetLocal(installableExtension.identifierWithVersion);
|
||||
const local = await this.unsetUninstalledAndGetLocal(installableExtension.key);
|
||||
if (local) {
|
||||
return installableExtension.metadata ? this.extensionsScanner.saveMetadataForLocalExtension(local, installableExtension.metadata) : local;
|
||||
return installableExtension.metadata ? this.extensionsScanner.updateMetadata(local, installableExtension.metadata) : local;
|
||||
}
|
||||
} catch (e) {
|
||||
if (isMacintosh) {
|
||||
@@ -247,28 +498,28 @@ abstract class AbstractInstallExtensionTask extends AbstractExtensionTask<ILocal
|
||||
return this.extract(installableExtension, token);
|
||||
}
|
||||
|
||||
protected async unsetUninstalledAndGetLocal(identifierWithVersion: ExtensionIdentifierWithVersion): Promise<ILocalExtension | null> {
|
||||
const isUninstalled = await this.isUninstalled(identifierWithVersion);
|
||||
protected async unsetUninstalledAndGetLocal(extensionKey: ExtensionKey): Promise<ILocalExtension | null> {
|
||||
const isUninstalled = await this.isUninstalled(extensionKey);
|
||||
if (!isUninstalled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.logService.trace('Removing the extension from uninstalled list:', identifierWithVersion.id);
|
||||
this.logService.trace('Removing the extension from uninstalled list:', extensionKey.id);
|
||||
// If the same version of extension is marked as uninstalled, remove it from there and return the local.
|
||||
const local = await this.extensionsScanner.setInstalled(identifierWithVersion);
|
||||
this.logService.info('Removed the extension from uninstalled list:', identifierWithVersion.id);
|
||||
const local = await this.extensionsScanner.setInstalled(extensionKey);
|
||||
this.logService.info('Removed the extension from uninstalled list:', extensionKey.id);
|
||||
|
||||
return local;
|
||||
}
|
||||
|
||||
private async isUninstalled(identifier: ExtensionIdentifierWithVersion): Promise<boolean> {
|
||||
private async isUninstalled(extensionId: ExtensionKey): Promise<boolean> {
|
||||
const uninstalled = await this.extensionsScanner.getUninstalledExtensions();
|
||||
return !!uninstalled[identifier.key()];
|
||||
return !!uninstalled[extensionId.toString()];
|
||||
}
|
||||
|
||||
private async extract({ zipPath, identifierWithVersion, metadata }: InstallableExtension, token: CancellationToken): Promise<ILocalExtension> {
|
||||
let local = await this.extensionsScanner.extractUserExtension(identifierWithVersion, zipPath, metadata, token);
|
||||
this.logService.info('Extracting completed.', identifierWithVersion.id);
|
||||
private async extract({ zipPath, key, metadata }: InstallableExtension, token: CancellationToken): Promise<ILocalExtension> {
|
||||
let local = await this.extensionsScanner.extractUserExtension(key, zipPath, metadata, token);
|
||||
this.logService.info('Extracting completed.', key.id);
|
||||
return local;
|
||||
}
|
||||
|
||||
@@ -278,12 +529,12 @@ class InstallGalleryExtensionTask extends AbstractInstallExtensionTask {
|
||||
|
||||
constructor(
|
||||
private readonly gallery: IGalleryExtension,
|
||||
private readonly options: InstallOptions,
|
||||
options: InstallOptions,
|
||||
private readonly extensionsDownloader: ExtensionsDownloader,
|
||||
extensionsScanner: ExtensionsScanner,
|
||||
logService: ILogService,
|
||||
) {
|
||||
super(gallery.identifier, gallery, extensionsScanner, logService);
|
||||
super(gallery.identifier, gallery, options, extensionsScanner, logService);
|
||||
}
|
||||
|
||||
protected async doRun(token: CancellationToken): Promise<ILocalExtension> {
|
||||
@@ -296,10 +547,17 @@ class InstallGalleryExtensionTask extends AbstractInstallExtensionTask {
|
||||
const installableExtension = await this.downloadInstallableExtension(this.gallery, this._operation);
|
||||
installableExtension.metadata.isMachineScoped = this.options.isMachineScoped || existingExtension?.isMachineScoped;
|
||||
installableExtension.metadata.isBuiltin = this.options.isBuiltin || existingExtension?.isBuiltin;
|
||||
installableExtension.metadata.isSystem = existingExtension?.type === ExtensionType.System ? true : undefined;
|
||||
installableExtension.metadata.updated = !!existingExtension;
|
||||
installableExtension.metadata.isPreReleaseVersion = this.gallery.properties.isPreReleaseVersion;
|
||||
installableExtension.metadata.preRelease = this.gallery.properties.isPreReleaseVersion ||
|
||||
(isBoolean(this.options.installPreReleaseVersion)
|
||||
? this.options.installPreReleaseVersion /* Respect the passed flag */
|
||||
: existingExtension?.preRelease /* Respect the existing pre-release flag if it was set */);
|
||||
|
||||
try {
|
||||
const local = await this.installExtension(installableExtension, token);
|
||||
if (existingExtension && semver.neq(existingExtension.manifest.version, this.gallery.version)) {
|
||||
if (existingExtension && (existingExtension.targetPlatform !== local.targetPlatform || semver.neq(existingExtension.manifest.version, local.manifest.version))) {
|
||||
await this.extensionsScanner.setUninstalled(existingExtension);
|
||||
}
|
||||
return local;
|
||||
@@ -323,6 +581,7 @@ class InstallGalleryExtensionTask extends AbstractInstallExtensionTask {
|
||||
id: extension.identifier.uuid,
|
||||
publisherId: extension.publisherId,
|
||||
publisherDisplayName: extension.publisherDisplayName,
|
||||
targetPlatform: extension.properties.targetPlatform
|
||||
};
|
||||
|
||||
let zipPath: string | undefined;
|
||||
@@ -335,8 +594,8 @@ class InstallGalleryExtensionTask extends AbstractInstallExtensionTask {
|
||||
}
|
||||
|
||||
try {
|
||||
const manifest = await getManifest(zipPath);
|
||||
return (<Required<InstallableExtension>>{ zipPath, identifierWithVersion: new ExtensionIdentifierWithVersion(extension.identifier, manifest.version), metadata });
|
||||
await getManifest(zipPath);
|
||||
return (<Required<InstallableExtension>>{ zipPath, key: ExtensionKey.create(extension), metadata });
|
||||
} catch (error) {
|
||||
await this.deleteDownloadedVSIX(zipPath);
|
||||
throw new ExtensionManagementError(joinErrors(error).message, ExtensionManagementErrorCode.Invalid);
|
||||
@@ -349,25 +608,25 @@ class InstallVSIXTask extends AbstractInstallExtensionTask {
|
||||
constructor(
|
||||
private readonly manifest: IExtensionManifest,
|
||||
private readonly location: URI,
|
||||
private readonly options: InstallOptions,
|
||||
options: InstallOptions,
|
||||
private readonly galleryService: IExtensionGalleryService,
|
||||
extensionsScanner: ExtensionsScanner,
|
||||
logService: ILogService
|
||||
) {
|
||||
super({ id: getGalleryExtensionId(manifest.publisher, manifest.name) }, location, extensionsScanner, logService);
|
||||
super({ id: getGalleryExtensionId(manifest.publisher, manifest.name) }, location, options, extensionsScanner, logService);
|
||||
}
|
||||
|
||||
protected async doRun(token: CancellationToken): Promise<ILocalExtension> {
|
||||
const identifierWithVersion = new ExtensionIdentifierWithVersion(this.identifier, this.manifest.version);
|
||||
const extensionKey = new ExtensionKey(this.identifier, this.manifest.version);
|
||||
const installedExtensions = await this.extensionsScanner.scanExtensions(ExtensionType.User);
|
||||
const existing = installedExtensions.find(i => areSameExtensions(this.identifier, i.identifier));
|
||||
const metadata = await this.getMetadata(this.identifier.id, token);
|
||||
const metadata = await this.getMetadata(this.identifier.id, this.manifest.version, token);
|
||||
metadata.isMachineScoped = this.options.isMachineScoped || existing?.isMachineScoped;
|
||||
metadata.isBuiltin = this.options.isBuiltin || existing?.isBuiltin;
|
||||
|
||||
if (existing) {
|
||||
this._operation = InstallOperation.Update;
|
||||
if (identifierWithVersion.equals(new ExtensionIdentifierWithVersion(existing.identifier, existing.manifest.version))) {
|
||||
if (extensionKey.equals(new ExtensionKey(existing.identifier, existing.manifest.version))) {
|
||||
try {
|
||||
await this.extensionsScanner.removeExtension(existing, 'existing');
|
||||
} catch (e) {
|
||||
@@ -379,7 +638,7 @@ class InstallVSIXTask extends AbstractInstallExtensionTask {
|
||||
} 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);
|
||||
const existing = await this.unsetUninstalledAndGetLocal(extensionKey);
|
||||
if (existing) {
|
||||
try {
|
||||
await this.extensionsScanner.removeExtension(existing, 'existing');
|
||||
@@ -389,14 +648,23 @@ class InstallVSIXTask extends AbstractInstallExtensionTask {
|
||||
}
|
||||
}
|
||||
|
||||
return this.installExtension({ zipPath: path.resolve(this.location.fsPath), identifierWithVersion, metadata }, token);
|
||||
return this.installExtension({ zipPath: path.resolve(this.location.fsPath), key: extensionKey, metadata }, token);
|
||||
}
|
||||
|
||||
private async getMetadata(name: string, token: CancellationToken): Promise<IMetadata> {
|
||||
private async getMetadata(id: string, version: string, token: CancellationToken): Promise<Metadata> {
|
||||
try {
|
||||
const galleryExtension = (await this.galleryService.query({ names: [name], pageSize: 1 }, token)).firstPage[0];
|
||||
let [galleryExtension] = await this.galleryService.getExtensions([{ id, version }], token);
|
||||
if (!galleryExtension) {
|
||||
[galleryExtension] = await this.galleryService.getExtensions([{ id }], token);
|
||||
}
|
||||
if (galleryExtension) {
|
||||
return { id: galleryExtension.identifier.uuid, publisherDisplayName: galleryExtension.publisherDisplayName, publisherId: galleryExtension.publisherId };
|
||||
return {
|
||||
id: galleryExtension.identifier.uuid,
|
||||
publisherDisplayName: galleryExtension.publisherDisplayName,
|
||||
publisherId: galleryExtension.publisherId,
|
||||
isPreReleaseVersion: galleryExtension.properties.isPreReleaseVersion,
|
||||
preRelease: galleryExtension.properties.isPreReleaseVersion || this.options.installPreReleaseVersion
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
/* Ignore Error */
|
||||
@@ -417,8 +685,8 @@ class UninstallExtensionTask extends AbstractExtensionTask<void> implements IUni
|
||||
const toUninstall: ILocalExtension[] = [];
|
||||
const userExtensions = await this.extensionsScanner.scanUserExtensions(false);
|
||||
if (this.options.versionOnly) {
|
||||
const extensionIdentifierWithVersion = new ExtensionIdentifierWithVersion(this.extension.identifier, this.extension.manifest.version);
|
||||
toUninstall.push(...userExtensions.filter(u => extensionIdentifierWithVersion.equals(new ExtensionIdentifierWithVersion(u.identifier, u.manifest.version))));
|
||||
const extensionKey = ExtensionKey.create(this.extension);
|
||||
toUninstall.push(...userExtensions.filter(u => extensionKey.equals(ExtensionKey.create(u))));
|
||||
} else {
|
||||
toUninstall.push(...userExtensions.filter(u => areSameExtensions(u.identifier, this.extension.identifier)));
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { join, } from 'vs/base/common/path';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { env as processEnv } from 'vs/base/common/process';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { IExecutableBasedExtensionTip } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { forEach } 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';
|
||||
|
||||
type IExeBasedExtensionTips = {
|
||||
readonly exeFriendlyName: string,
|
||||
readonly windowsPath?: string,
|
||||
readonly recommendations: { extensionId: string, extensionName: string, isExtensionPack: boolean }[];
|
||||
};
|
||||
|
||||
export class ExtensionTipsService extends BaseExtensionTipsService {
|
||||
|
||||
private readonly allImportantExecutableTips: Map<string, IExeBasedExtensionTips> = new Map<string, IExeBasedExtensionTips>();
|
||||
private readonly allOtherExecutableTips: Map<string, IExeBasedExtensionTips> = new Map<string, IExeBasedExtensionTips>();
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService private readonly environmentService: INativeEnvironmentService,
|
||||
@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 }) => {
|
||||
const importantRecommendations: { extensionId: string, extensionName: string, isExtensionPack: boolean }[] = [];
|
||||
const otherRecommendations: { extensionId: string, extensionName: string, isExtensionPack: boolean }[] = [];
|
||||
forEach(value.recommendations, ({ key: extensionId, value }) => {
|
||||
if (value.important) {
|
||||
importantRecommendations.push({ extensionId, extensionName: value.name, isExtensionPack: !!value.isExtensionPack });
|
||||
} else {
|
||||
otherRecommendations.push({ extensionId, extensionName: value.name, isExtensionPack: !!value.isExtensionPack });
|
||||
}
|
||||
});
|
||||
if (importantRecommendations.length) {
|
||||
this.allImportantExecutableTips.set(key, { exeFriendlyName: value.friendlyName, windowsPath: value.windowsPath, recommendations: importantRecommendations });
|
||||
}
|
||||
if (otherRecommendations.length) {
|
||||
this.allOtherExecutableTips.set(key, { exeFriendlyName: value.friendlyName, windowsPath: value.windowsPath, recommendations: otherRecommendations });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
override getImportantExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
|
||||
return this.getValidExecutableBasedExtensionTips(this.allImportantExecutableTips);
|
||||
}
|
||||
|
||||
override getOtherExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
|
||||
return this.getValidExecutableBasedExtensionTips(this.allOtherExecutableTips);
|
||||
}
|
||||
|
||||
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%', processEnv['USERPROFILE']!)
|
||||
.replace('%ProgramFiles(x86)%', processEnv['ProgramFiles(x86)']!)
|
||||
.replace('%ProgramFiles%', processEnv['ProgramFiles']!)
|
||||
.replace('%APPDATA%', processEnv['APPDATA']!)
|
||||
.replace('%WINDIR%', processEnv['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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,422 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { flatten } from 'vs/base/common/arrays';
|
||||
import { Limiter, Promises, Queue } from 'vs/base/common/async';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { FileAccess } from 'vs/base/common/network';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { basename } from 'vs/base/common/resources';
|
||||
import * as semver from 'vs/base/common/semver/semver';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { extract, ExtractError } from 'vs/base/node/zip';
|
||||
import { localize } from 'vs/nls';
|
||||
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { ExtensionManagementError, ExtensionManagementErrorCode, IGalleryMetadata, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { areSameExtensions, ExtensionIdentifierWithVersion, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls';
|
||||
import { ExtensionType, IExtensionIdentifier, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { CancellationToken } from 'vscode';
|
||||
|
||||
export type IMetadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; isBuiltin: boolean; }>;
|
||||
type IStoredMetadata = IMetadata & { installedTimestamp: number | undefined };
|
||||
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>,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@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, ExtensionManagementErrorCode.Internal))));
|
||||
}
|
||||
|
||||
if (type === null || type === ExtensionType.User) {
|
||||
promises.push(this.scanUserExtensions(true).then(null, e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, ExtensionManagementErrorCode.Internal))));
|
||||
}
|
||||
|
||||
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, metadata: IMetadata | undefined, token: CancellationToken): Promise<ILocalExtension> {
|
||||
const folderName = identifierWithVersion.key();
|
||||
const tempPath = path.join(this.extensionsPath, `.${generateUuid()}`);
|
||||
const extensionPath = path.join(this.extensionsPath, folderName);
|
||||
|
||||
try {
|
||||
await pfs.Promises.rm(extensionPath);
|
||||
} catch (error) {
|
||||
try {
|
||||
await pfs.Promises.rm(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), ExtensionManagementErrorCode.Delete);
|
||||
}
|
||||
|
||||
await this.extractAtLocation(identifierWithVersion, zipPath, tempPath, token);
|
||||
let local = await this.scanExtension(URI.file(tempPath), ExtensionType.User);
|
||||
if (!local) {
|
||||
throw new Error(localize('cannot read', "Cannot read the extension from {0}", tempPath));
|
||||
}
|
||||
await this.storeMetadata(local, { ...metadata, installedTimestamp: Date.now() });
|
||||
|
||||
try {
|
||||
await this.rename(identifierWithVersion, tempPath, extensionPath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */);
|
||||
this.logService.info('Renamed to', extensionPath);
|
||||
} catch (error) {
|
||||
try {
|
||||
await pfs.Promises.rm(tempPath);
|
||||
} catch (e) { /* ignore */ }
|
||||
if (error.code === 'ENOTEMPTY') {
|
||||
this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, identifierWithVersion.id);
|
||||
} else {
|
||||
this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted from extracted location`, tempPath);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
local = await this.scanExtension(URI.file(extensionPath), 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);
|
||||
await this.storeMetadata(local, { ...metadata, installedTimestamp: local.installedTimestamp });
|
||||
return local;
|
||||
}
|
||||
|
||||
private async storeMetadata(local: ILocalExtension, storedMetadata: IStoredMetadata): Promise<ILocalExtension> {
|
||||
// unset if false
|
||||
storedMetadata.isMachineScoped = storedMetadata.isMachineScoped || undefined;
|
||||
storedMetadata.isBuiltin = storedMetadata.isBuiltin || undefined;
|
||||
storedMetadata.installedTimestamp = storedMetadata.installedTimestamp || undefined;
|
||||
const manifestPath = path.join(local.location.fsPath, 'package.json');
|
||||
const raw = await pfs.Promises.readFile(manifestPath, 'utf8');
|
||||
const { manifest } = await this.parseManifest(raw);
|
||||
(manifest as ILocalExtensionManifest).__metadata = storedMetadata;
|
||||
await pfs.Promises.writeFile(manifestPath, JSON.stringify(manifest, null, '\t'));
|
||||
return local;
|
||||
}
|
||||
|
||||
getUninstalledExtensions(): Promise<IStringDictionary<boolean>> {
|
||||
return this.withUninstalledExtensions();
|
||||
}
|
||||
|
||||
async setUninstalled(...extensions: ILocalExtension[]): Promise<void> {
|
||||
const ids: ExtensionIdentifierWithVersion[] = extensions.map(e => new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version));
|
||||
await this.withUninstalledExtensions(uninstalled => {
|
||||
ids.forEach(id => uninstalled[id.key()] = true);
|
||||
});
|
||||
}
|
||||
|
||||
async setInstalled(identifierWithVersion: ExtensionIdentifierWithVersion): Promise<ILocalExtension | null> {
|
||||
await this.withUninstalledExtensions(uninstalled => delete uninstalled[identifierWithVersion.key()]);
|
||||
const installed = await this.scanExtensions(ExtensionType.User);
|
||||
const localExtension = installed.find(i => new ExtensionIdentifierWithVersion(i.identifier, i.manifest.version).equals(identifierWithVersion)) || null;
|
||||
if (!localExtension) {
|
||||
return null;
|
||||
}
|
||||
await this.storeMetadata(localExtension, { installedTimestamp: Date.now() });
|
||||
return this.scanExtension(localExtension.location, ExtensionType.User);
|
||||
}
|
||||
|
||||
private async withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary<boolean>) => void): Promise<IStringDictionary<boolean>> {
|
||||
return this.uninstalledFileLimiter.queue(async () => {
|
||||
let raw: string | undefined;
|
||||
try {
|
||||
raw = await pfs.Promises.readFile(this.uninstalledPath, 'utf8');
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
let uninstalled = {};
|
||||
if (raw) {
|
||||
try {
|
||||
uninstalled = JSON.parse(raw);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
if (updateFn) {
|
||||
updateFn(uninstalled);
|
||||
if (Object.keys(uninstalled).length) {
|
||||
await pfs.Promises.writeFile(this.uninstalledPath, JSON.stringify(uninstalled));
|
||||
} else {
|
||||
await pfs.Promises.rm(this.uninstalledPath);
|
||||
}
|
||||
}
|
||||
|
||||
return uninstalled;
|
||||
});
|
||||
}
|
||||
|
||||
async removeExtension(extension: ILocalExtension, type: string): Promise<void> {
|
||||
this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id, extension.location.fsPath);
|
||||
await pfs.Promises.rm(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.Promises.rm(location);
|
||||
} catch (e) {
|
||||
throw new ExtensionManagementError(this.joinErrors(e).message, ExtensionManagementErrorCode.Delete);
|
||||
}
|
||||
|
||||
try {
|
||||
await extract(zipPath, location, { sourcePath: 'extension', overwrite: true }, token);
|
||||
this.logService.info(`Extracted extension to ${location}:`, identifier.id);
|
||||
} catch (e) {
|
||||
try { await pfs.Promises.rm(location); } catch (e) { /* Ignore */ }
|
||||
let errorCode = ExtensionManagementErrorCode.Extract;
|
||||
if (e instanceof ExtractError) {
|
||||
if (e.type === 'CorruptZip') {
|
||||
errorCode = ExtensionManagementErrorCode.CorruptZip;
|
||||
} else if (e.type === 'Incomplete') {
|
||||
errorCode = ExtensionManagementErrorCode.IncompleteZip;
|
||||
}
|
||||
}
|
||||
throw new ExtensionManagementError(e.message, errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
private async rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise<void> {
|
||||
try {
|
||||
await pfs.Promises.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 || ExtensionManagementErrorCode.Rename);
|
||||
}
|
||||
}
|
||||
|
||||
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 stat = await this.fileService.resolve(URI.file(dir));
|
||||
if (stat.children) {
|
||||
const extensions = await Promise.all<ILocalExtension>(stat.children.filter(c => c.isDirectory)
|
||||
.map(c => limiter.queue(async () => {
|
||||
if (type === ExtensionType.User && basename(c.resource).indexOf('.') === 0) { // Do not consider user extension folder starting with `.`
|
||||
return null;
|
||||
}
|
||||
return this.scanExtension(c.resource, type);
|
||||
})));
|
||||
return extensions.filter(e => e && e.identifier);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private async scanExtension(extensionLocation: URI, type: ExtensionType): Promise<ILocalExtension | null> {
|
||||
try {
|
||||
const stat = await this.fileService.resolve(extensionLocation);
|
||||
if (stat.children) {
|
||||
const { manifest, metadata } = await this.readManifest(extensionLocation.fsPath);
|
||||
const readmeUrl = stat.children.find(({ name }) => /^readme(\.txt|\.md|)$/i.test(name))?.resource;
|
||||
const changelogUrl = stat.children.find(({ name }) => /^changelog(\.txt|\.md|)$/i.test(name))?.resource;
|
||||
const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };
|
||||
const local = <ILocalExtension>{ type, identifier, manifest, location: extensionLocation, readmeUrl, changelogUrl, publisherDisplayName: null, publisherId: null, isMachineScoped: false, isBuiltin: type === ExtensionType.System };
|
||||
if (metadata) {
|
||||
this.setMetadata(local, metadata);
|
||||
local.installedTimestamp = metadata.installedTimestamp;
|
||||
}
|
||||
return local;
|
||||
}
|
||||
} catch (e) {
|
||||
if (type !== ExtensionType.System) {
|
||||
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 Promises.settled(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 Promises.settled(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 Promises.settled(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: IStoredMetadata | null; }> {
|
||||
const promises = [
|
||||
pfs.Promises.readFile(path.join(extensionPath, 'package.json'), 'utf8')
|
||||
.then(raw => this.parseManifest(raw)),
|
||||
pfs.Promises.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,29 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IExtensionsScannerService, NativeExtensionsScannerService, } from 'vs/platform/extensionManagement/common/extensionsScannerService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
|
||||
export class ExtensionsScannerService extends NativeExtensionsScannerService implements IExtensionsScannerService {
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@ILogService logService: ILogService,
|
||||
@INativeEnvironmentService environmentService: INativeEnvironmentService,
|
||||
@IProductService productService: IProductService,
|
||||
) {
|
||||
super(
|
||||
URI.file(environmentService.builtinExtensionsPath),
|
||||
URI.file(environmentService.extensionsPath),
|
||||
environmentService.userHome,
|
||||
URI.file(environmentService.userDataPath),
|
||||
fileService, logService, environmentService, productService);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,18 +5,18 @@
|
||||
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ExtUri } from 'vs/base/common/resources';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { DidUninstallExtensionEvent, IExtensionManagementService, ILocalExtension, InstallExtensionEvent, InstallExtensionResult } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { FileChangeType, FileSystemProviderCapabilities, IFileChange, IFileService } from 'vs/platform/files/common/files';
|
||||
import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { FileChangeType, IFileChange, IFileService } from 'vs/platform/files/common/files';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
|
||||
|
||||
export class ExtensionsWatcher extends Disposable {
|
||||
|
||||
private readonly _onDidChangeExtensionsByAnotherSource = this._register(new Emitter<{ added: ILocalExtension[], removed: IExtensionIdentifier[] }>());
|
||||
private readonly _onDidChangeExtensionsByAnotherSource = this._register(new Emitter<{ added: ILocalExtension[]; removed: IExtensionIdentifier[] }>());
|
||||
readonly onDidChangeExtensionsByAnotherSource = this._onDidChangeExtensionsByAnotherSource.event;
|
||||
|
||||
private startTimestamp = 0;
|
||||
@@ -28,9 +28,10 @@ export class ExtensionsWatcher extends Disposable {
|
||||
@IFileService fileService: IFileService,
|
||||
@INativeEnvironmentService environmentService: INativeEnvironmentService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
|
||||
) {
|
||||
super();
|
||||
this.extensionsManagementService.getInstalled(ExtensionType.User).then(extensions => {
|
||||
this.extensionsManagementService.getInstalled().then(extensions => {
|
||||
this.installedExtensions = extensions.map(e => e.identifier);
|
||||
this.startTimestamp = Date.now();
|
||||
});
|
||||
@@ -39,29 +40,28 @@ export class ExtensionsWatcher extends Disposable {
|
||||
this._register(extensionsManagementService.onDidUninstallExtension(e => this.onDidUninstallExtension(e)));
|
||||
|
||||
const extensionsResource = URI.file(environmentService.extensionsPath);
|
||||
const extUri = new ExtUri(resource => !fileService.hasCapability(resource, FileSystemProviderCapabilities.PathCaseSensitive));
|
||||
this._register(fileService.watch(extensionsResource));
|
||||
this._register(Event.filter(fileService.onDidChangeFilesRaw, e => e.changes.some(change => this.doesChangeAffects(change, extensionsResource, extUri)))(() => this.onDidChange()));
|
||||
this._register(Event.filter(fileService.onDidFilesChange, e => e.rawChanges.some(change => this.doesChangeAffects(change, extensionsResource)))(() => this.onDidChange()));
|
||||
}
|
||||
|
||||
private doesChangeAffects(change: IFileChange, extensionsResource: URI, extUri: ExtUri): boolean {
|
||||
// Is not immediate child of extensions resource
|
||||
if (!extUri.isEqual(extUri.dirname(change.resource), extensionsResource)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// .obsolete file changed
|
||||
if (extUri.isEqual(change.resource, extUri.joinPath(extensionsResource, '.obsolete'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
private doesChangeAffects(change: IFileChange, extensionsResource: URI): boolean {
|
||||
// Only interested in added/deleted changes
|
||||
if (change.type !== FileChangeType.ADDED && change.type !== FileChangeType.DELETED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ingore changes to files starting with `.`
|
||||
if (extUri.basename(change.resource).startsWith('.')) {
|
||||
// Is not immediate child of extensions resource
|
||||
if (!this.uriIdentityService.extUri.isEqual(this.uriIdentityService.extUri.dirname(change.resource), extensionsResource)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// .obsolete file changed
|
||||
if (this.uriIdentityService.extUri.isEqual(change.resource, this.uriIdentityService.extUri.joinPath(extensionsResource, '.obsolete'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ignore changes to files starting with `.`
|
||||
if (this.uriIdentityService.extUri.basename(change.resource).startsWith('.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ export class ExtensionsWatcher extends Disposable {
|
||||
|
||||
private async onDidChange(): Promise<void> {
|
||||
if (this.installedExtensions) {
|
||||
const extensions = await this.extensionsManagementService.getInstalled(ExtensionType.User);
|
||||
const extensions = await this.extensionsManagementService.getInstalled();
|
||||
const added = extensions.filter(e => {
|
||||
if ([...this.installingExtensions, ...this.installedExtensions!].some(identifier => areSameExtensions(identifier, e.identifier))) {
|
||||
return false;
|
||||
|
||||
@@ -12,16 +12,17 @@ import { mock } from 'vs/base/test/common/mock';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IRawGalleryExtensionVersion, resolveMarketplaceHeaders, sortExtensionVersions } from 'vs/platform/extensionManagement/common/extensionGalleryService';
|
||||
import { TargetPlatform } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IRawGalleryExtensionVersion, sortExtensionVersions } from 'vs/platform/extensionManagement/common/extensionGalleryService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { resolveMarketplaceHeaders } from 'vs/platform/externalServices/common/marketplace';
|
||||
import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { TelemetryConfiguration, TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { TargetPlatform } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
class EnvironmentServiceMock extends mock<IEnvironmentService>() {
|
||||
override readonly serviceMachineIdResource: URI;
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import { EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { ExtensionKey } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { TargetPlatform } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
suite('Extension Identifier Pattern', () => {
|
||||
|
||||
@@ -26,4 +28,17 @@ suite('Extension Identifier Pattern', () => {
|
||||
assert.strictEqual(false, regEx.test('publ_isher.name'));
|
||||
assert.strictEqual(false, regEx.test('publisher._name'));
|
||||
});
|
||||
|
||||
test('extension key', () => {
|
||||
assert.strictEqual(new ExtensionKey({ id: 'pub.extension-name' }, '1.0.1').toString(), 'pub.extension-name-1.0.1');
|
||||
assert.strictEqual(new ExtensionKey({ id: 'pub.extension-name' }, '1.0.1', TargetPlatform.UNDEFINED).toString(), 'pub.extension-name-1.0.1');
|
||||
assert.strictEqual(new ExtensionKey({ id: 'pub.extension-name' }, '1.0.1', TargetPlatform.WIN32_IA32).toString(), `pub.extension-name-1.0.1-${TargetPlatform.WIN32_IA32}`);
|
||||
});
|
||||
|
||||
test('extension key parsing', () => {
|
||||
assert.strictEqual(ExtensionKey.parse('pub.extension-name'), null);
|
||||
assert.strictEqual(ExtensionKey.parse('pub.extension-name@1.2.3'), null);
|
||||
assert.strictEqual(ExtensionKey.parse('pub.extension-name-1.0.1')?.toString(), 'pub.extension-name-1.0.1');
|
||||
assert.strictEqual(ExtensionKey.parse('pub.extension-name-1.0.1-win32-ia32')?.toString(), 'pub.extension-name-1.0.1-win32-ia32');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { dirname, joinPath } from 'vs/base/common/resources';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { AbstractExtensionsScannerService, IExtensionsScannerService, IScannedExtensionManifest, Translations } from 'vs/platform/extensionManagement/common/extensionsScannerService';
|
||||
import { ExtensionType, IExtensionManifest, MANIFEST_CACHE_FOLDER, TargetPlatform } from 'vs/platform/extensions/common/extensions';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { ILogService, NullLogService } from 'vs/platform/log/common/log';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
|
||||
let translations: Translations = Object.create(null);
|
||||
const ROOT = URI.file('/ROOT');
|
||||
|
||||
class ExtensionsScannerService extends AbstractExtensionsScannerService implements IExtensionsScannerService {
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@ILogService logService: ILogService,
|
||||
@INativeEnvironmentService nativeEnvironmentService: INativeEnvironmentService,
|
||||
@IProductService productService: IProductService,
|
||||
) {
|
||||
super(
|
||||
URI.file(nativeEnvironmentService.builtinExtensionsPath),
|
||||
URI.file(nativeEnvironmentService.extensionsPath),
|
||||
joinPath(nativeEnvironmentService.userHome, '.vscode-oss-dev', 'extensions', 'control.json'),
|
||||
joinPath(ROOT, MANIFEST_CACHE_FOLDER),
|
||||
fileService, logService, nativeEnvironmentService, productService);
|
||||
}
|
||||
|
||||
protected async getTranslations(language: string): Promise<Translations> {
|
||||
return translations;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
suite('NativeExtensionsScanerService Test', () => {
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
let instantiationService: TestInstantiationService;
|
||||
|
||||
setup(async () => {
|
||||
translations = {};
|
||||
instantiationService = new TestInstantiationService();
|
||||
const logService = new NullLogService();
|
||||
const fileService = disposables.add(new FileService(logService));
|
||||
const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider());
|
||||
fileService.registerProvider(ROOT.scheme, fileSystemProvider);
|
||||
instantiationService.stub(ILogService, logService);
|
||||
instantiationService.stub(IFileService, fileService);
|
||||
const systemExtensionsLocation = joinPath(ROOT, 'system');
|
||||
const userExtensionsLocation = joinPath(ROOT, 'extensions');
|
||||
instantiationService.stub(INativeEnvironmentService, {
|
||||
userHome: ROOT,
|
||||
builtinExtensionsPath: systemExtensionsLocation.fsPath,
|
||||
extensionsPath: userExtensionsLocation.fsPath,
|
||||
});
|
||||
instantiationService.stub(IProductService, { version: '1.66.0' });
|
||||
await fileService.createFolder(systemExtensionsLocation);
|
||||
await fileService.createFolder(userExtensionsLocation);
|
||||
});
|
||||
|
||||
teardown(() => disposables.clear());
|
||||
|
||||
test('scan system extension', async () => {
|
||||
const manifest: Partial<IExtensionManifest> = anExtensionManifest({ 'name': 'name', 'publisher': 'pub' });
|
||||
const extensionLocation = await aSystemExtension(manifest);
|
||||
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
|
||||
|
||||
const actual = await testObject.scanSystemExtensions({});
|
||||
|
||||
assert.deepStrictEqual(actual.length, 1);
|
||||
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
|
||||
assert.deepStrictEqual(actual[0].location.toString(), extensionLocation.toString());
|
||||
assert.deepStrictEqual(actual[0].isBuiltin, true);
|
||||
assert.deepStrictEqual(actual[0].type, ExtensionType.System);
|
||||
assert.deepStrictEqual(actual[0].isValid, true);
|
||||
assert.deepStrictEqual(actual[0].validations, []);
|
||||
assert.deepStrictEqual(actual[0].metadata, undefined);
|
||||
assert.deepStrictEqual(actual[0].targetPlatform, TargetPlatform.UNDEFINED);
|
||||
assert.deepStrictEqual(actual[0].manifest, manifest);
|
||||
});
|
||||
|
||||
test('scan user extension', async () => {
|
||||
const manifest: Partial<IScannedExtensionManifest> = anExtensionManifest({ 'name': 'name', 'publisher': 'pub', __metadata: { id: 'uuid' } });
|
||||
const extensionLocation = await aUserExtension(manifest);
|
||||
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
|
||||
|
||||
const actual = await testObject.scanUserExtensions({});
|
||||
|
||||
assert.deepStrictEqual(actual.length, 1);
|
||||
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name', uuid: 'uuid' });
|
||||
assert.deepStrictEqual(actual[0].location.toString(), extensionLocation.toString());
|
||||
assert.deepStrictEqual(actual[0].isBuiltin, false);
|
||||
assert.deepStrictEqual(actual[0].type, ExtensionType.User);
|
||||
assert.deepStrictEqual(actual[0].isValid, true);
|
||||
assert.deepStrictEqual(actual[0].validations, []);
|
||||
assert.deepStrictEqual(actual[0].metadata, { id: 'uuid' });
|
||||
assert.deepStrictEqual(actual[0].targetPlatform, TargetPlatform.UNDEFINED);
|
||||
delete manifest.__metadata;
|
||||
assert.deepStrictEqual(actual[0].manifest, manifest);
|
||||
});
|
||||
|
||||
test('scan existing extension', async () => {
|
||||
const manifest: Partial<IExtensionManifest> = anExtensionManifest({ 'name': 'name', 'publisher': 'pub' });
|
||||
const extensionLocation = await aUserExtension(manifest);
|
||||
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
|
||||
|
||||
const actual = await testObject.scanExistingExtension(extensionLocation, ExtensionType.User, {});
|
||||
|
||||
assert.notEqual(actual, null);
|
||||
assert.deepStrictEqual(actual!.identifier, { id: 'pub.name' });
|
||||
assert.deepStrictEqual(actual!.location.toString(), extensionLocation.toString());
|
||||
assert.deepStrictEqual(actual!.isBuiltin, false);
|
||||
assert.deepStrictEqual(actual!.type, ExtensionType.User);
|
||||
assert.deepStrictEqual(actual!.isValid, true);
|
||||
assert.deepStrictEqual(actual!.validations, []);
|
||||
assert.deepStrictEqual(actual!.metadata, undefined);
|
||||
assert.deepStrictEqual(actual!.targetPlatform, TargetPlatform.UNDEFINED);
|
||||
assert.deepStrictEqual(actual!.manifest, manifest);
|
||||
});
|
||||
|
||||
test('scan single extension', async () => {
|
||||
const manifest: Partial<IExtensionManifest> = anExtensionManifest({ 'name': 'name', 'publisher': 'pub' });
|
||||
const extensionLocation = await aUserExtension(manifest);
|
||||
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
|
||||
|
||||
const actual = await testObject.scanOneOrMultipleExtensions(extensionLocation, ExtensionType.User, {});
|
||||
|
||||
assert.deepStrictEqual(actual.length, 1);
|
||||
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
|
||||
assert.deepStrictEqual(actual[0].location.toString(), extensionLocation.toString());
|
||||
assert.deepStrictEqual(actual[0].isBuiltin, false);
|
||||
assert.deepStrictEqual(actual[0].type, ExtensionType.User);
|
||||
assert.deepStrictEqual(actual[0].isValid, true);
|
||||
assert.deepStrictEqual(actual[0].validations, []);
|
||||
assert.deepStrictEqual(actual[0].metadata, undefined);
|
||||
assert.deepStrictEqual(actual[0].targetPlatform, TargetPlatform.UNDEFINED);
|
||||
assert.deepStrictEqual(actual[0].manifest, manifest);
|
||||
});
|
||||
|
||||
test('scan multiple extensions', async () => {
|
||||
const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }));
|
||||
await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' }));
|
||||
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
|
||||
|
||||
const actual = await testObject.scanOneOrMultipleExtensions(dirname(extensionLocation), ExtensionType.User, {});
|
||||
|
||||
assert.deepStrictEqual(actual.length, 2);
|
||||
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
|
||||
assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name2' });
|
||||
});
|
||||
|
||||
test('scan user extension with different versions', async () => {
|
||||
await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' }));
|
||||
await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2' }));
|
||||
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
|
||||
|
||||
const actual = await testObject.scanUserExtensions({});
|
||||
|
||||
assert.deepStrictEqual(actual.length, 1);
|
||||
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
|
||||
assert.deepStrictEqual(actual[0].manifest.version, '1.0.2');
|
||||
});
|
||||
|
||||
test('scan user extension include all versions', async () => {
|
||||
await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' }));
|
||||
await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2' }));
|
||||
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
|
||||
|
||||
const actual = await testObject.scanUserExtensions({ includeAllVersions: true });
|
||||
|
||||
assert.deepStrictEqual(actual.length, 2);
|
||||
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
|
||||
assert.deepStrictEqual(actual[0].manifest.version, '1.0.1');
|
||||
assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name' });
|
||||
assert.deepStrictEqual(actual[1].manifest.version, '1.0.2');
|
||||
});
|
||||
|
||||
test.skip('scan user extension with different versions and higher version is not compatible', async () => {
|
||||
await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' }));
|
||||
await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2', engines: { vscode: '^1.67.0' } }));
|
||||
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
|
||||
|
||||
const actual = await testObject.scanUserExtensions({});
|
||||
|
||||
assert.deepStrictEqual(actual.length, 1);
|
||||
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
|
||||
assert.deepStrictEqual(actual[0].manifest.version, '1.0.1');
|
||||
});
|
||||
|
||||
test.skip('scan exclude invalid extensions', async () => {
|
||||
await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }));
|
||||
await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub', engines: { vscode: '^1.67.0' } }));
|
||||
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
|
||||
|
||||
const actual = await testObject.scanUserExtensions({});
|
||||
|
||||
assert.deepStrictEqual(actual.length, 1);
|
||||
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
|
||||
});
|
||||
|
||||
test('scan exclude uninstalled extensions', async () => {
|
||||
await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }));
|
||||
await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' }));
|
||||
await instantiationService.get(IFileService).writeFile(joinPath(URI.file(instantiationService.get(INativeEnvironmentService).extensionsPath), '.obsolete'), VSBuffer.fromString(JSON.stringify({ 'pub.name2-1.0.0': true })));
|
||||
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
|
||||
|
||||
const actual = await testObject.scanUserExtensions({});
|
||||
|
||||
assert.deepStrictEqual(actual.length, 1);
|
||||
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
|
||||
});
|
||||
|
||||
test('scan include uninstalled extensions', async () => {
|
||||
await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }));
|
||||
await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' }));
|
||||
await instantiationService.get(IFileService).writeFile(joinPath(URI.file(instantiationService.get(INativeEnvironmentService).extensionsPath), '.obsolete'), VSBuffer.fromString(JSON.stringify({ 'pub.name2-1.0.0': true })));
|
||||
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
|
||||
|
||||
const actual = await testObject.scanUserExtensions({ includeUninstalled: true });
|
||||
|
||||
assert.deepStrictEqual(actual.length, 2);
|
||||
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
|
||||
assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name2' });
|
||||
});
|
||||
|
||||
test('scan include invalid extensions', async () => {
|
||||
await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }));
|
||||
await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub', engines: { vscode: '^1.67.0' } }));
|
||||
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
|
||||
|
||||
const actual = await testObject.scanUserExtensions({ includeInvalid: true });
|
||||
|
||||
assert.deepStrictEqual(actual.length, 2);
|
||||
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
|
||||
assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name2' });
|
||||
});
|
||||
|
||||
test('scan system extensions include additional builtin extensions', async () => {
|
||||
instantiationService.stub(IProductService, {
|
||||
version: '1.66.0',
|
||||
builtInExtensions: [
|
||||
{ name: 'pub.name2', version: '', repo: '', metadata: undefined },
|
||||
{ name: 'pub.name', version: '', repo: '', metadata: undefined }
|
||||
]
|
||||
});
|
||||
await anExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' }), joinPath(ROOT, 'additional'));
|
||||
const extensionLocation = await anExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }), joinPath(ROOT, 'additional'));
|
||||
await aSystemExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' }));
|
||||
await instantiationService.get(IFileService).writeFile(joinPath(instantiationService.get(INativeEnvironmentService).userHome, '.vscode-oss-dev', 'extensions', 'control.json'), VSBuffer.fromString(JSON.stringify({ 'pub.name2': 'disabled', 'pub.name': extensionLocation.fsPath })));
|
||||
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
|
||||
|
||||
const actual = await testObject.scanSystemExtensions({ checkControlFile: true });
|
||||
|
||||
assert.deepStrictEqual(actual.length, 1);
|
||||
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
|
||||
assert.deepStrictEqual(actual[0].manifest.version, '1.0.0');
|
||||
});
|
||||
|
||||
test('scan extension with default nls replacements', async () => {
|
||||
const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', displayName: '%displayName%' }));
|
||||
await instantiationService.get(IFileService).writeFile(joinPath(extensionLocation, 'package.nls.json'), VSBuffer.fromString(JSON.stringify({ displayName: 'Hello World' })));
|
||||
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
|
||||
|
||||
const actual = await testObject.scanUserExtensions({});
|
||||
|
||||
assert.deepStrictEqual(actual.length, 1);
|
||||
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
|
||||
assert.deepStrictEqual(actual[0].manifest.displayName, 'Hello World');
|
||||
});
|
||||
|
||||
test('scan extension with en nls replacements', async () => {
|
||||
const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', displayName: '%displayName%' }));
|
||||
await instantiationService.get(IFileService).writeFile(joinPath(extensionLocation, 'package.nls.json'), VSBuffer.fromString(JSON.stringify({ displayName: 'Hello World' })));
|
||||
const nlsLocation = joinPath(extensionLocation, 'package.en.json');
|
||||
await instantiationService.get(IFileService).writeFile(nlsLocation, VSBuffer.fromString(JSON.stringify({ contents: { package: { displayName: 'Hello World EN' } } })));
|
||||
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
|
||||
|
||||
translations = { 'pub.name': nlsLocation.fsPath };
|
||||
const actual = await testObject.scanUserExtensions({ language: 'en' });
|
||||
|
||||
assert.deepStrictEqual(actual.length, 1);
|
||||
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
|
||||
assert.deepStrictEqual(actual[0].manifest.displayName, 'Hello World EN');
|
||||
});
|
||||
|
||||
test('scan extension falls back to default nls replacements', async () => {
|
||||
const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', displayName: '%displayName%' }));
|
||||
await instantiationService.get(IFileService).writeFile(joinPath(extensionLocation, 'package.nls.json'), VSBuffer.fromString(JSON.stringify({ displayName: 'Hello World' })));
|
||||
const nlsLocation = joinPath(extensionLocation, 'package.en.json');
|
||||
await instantiationService.get(IFileService).writeFile(nlsLocation, VSBuffer.fromString(JSON.stringify({ contents: { package: { displayName: 'Hello World EN' } } })));
|
||||
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
|
||||
|
||||
translations = { 'pub.name2': nlsLocation.fsPath };
|
||||
const actual = await testObject.scanUserExtensions({ language: 'en' });
|
||||
|
||||
assert.deepStrictEqual(actual.length, 1);
|
||||
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
|
||||
assert.deepStrictEqual(actual[0].manifest.displayName, 'Hello World');
|
||||
});
|
||||
|
||||
async function aUserExtension(manifest: Partial<IScannedExtensionManifest>): Promise<URI> {
|
||||
const environmentService = instantiationService.get(INativeEnvironmentService);
|
||||
return anExtension(manifest, URI.file(environmentService.extensionsPath));
|
||||
}
|
||||
|
||||
async function aSystemExtension(manifest: Partial<IScannedExtensionManifest>): Promise<URI> {
|
||||
const environmentService = instantiationService.get(INativeEnvironmentService);
|
||||
return anExtension(manifest, URI.file(environmentService.builtinExtensionsPath));
|
||||
}
|
||||
|
||||
async function anExtension(manifest: Partial<IScannedExtensionManifest>, root: URI): Promise<URI> {
|
||||
const fileService = instantiationService.get(IFileService);
|
||||
const extensionLocation = joinPath(root, `${manifest.publisher}.${manifest.name}-${manifest.version}-${manifest.__metadata?.targetPlatform ?? TargetPlatform.UNDEFINED}`);
|
||||
await fileService.writeFile(joinPath(extensionLocation, 'package.json'), VSBuffer.fromString(JSON.stringify(manifest)));
|
||||
return extensionLocation;
|
||||
}
|
||||
|
||||
function anExtensionManifest(manifest: Partial<IScannedExtensionManifest>): Partial<IExtensionManifest> {
|
||||
return { engines: { vscode: '^1.66.0' }, version: '1.0.0', main: 'main.js', activationEvents: ['*'], ...manifest };
|
||||
}
|
||||
});
|
||||
@@ -12,7 +12,7 @@ export const IExtensionHostStarter = createDecorator<IExtensionHostStarter>('ext
|
||||
export const ipcExtensionHostStarterChannelName = 'extensionHostStarter';
|
||||
|
||||
export interface IExtensionHostProcessOptions {
|
||||
env: { [key: string]: string | undefined; };
|
||||
env: { [key: string]: string | undefined };
|
||||
detached: boolean;
|
||||
execArgv: string[] | undefined;
|
||||
silent: boolean;
|
||||
@@ -24,11 +24,11 @@ export interface IExtensionHostStarter {
|
||||
onDynamicStdout(id: string): Event<string>;
|
||||
onDynamicStderr(id: string): Event<string>;
|
||||
onDynamicMessage(id: string): Event<any>;
|
||||
onDynamicError(id: string): Event<{ error: SerializedError; }>;
|
||||
onDynamicError(id: string): Event<{ error: SerializedError }>;
|
||||
onDynamicExit(id: string): Event<{ code: number; signal: string }>;
|
||||
|
||||
createExtensionHost(): Promise<{ id: string; }>;
|
||||
start(id: string, opts: IExtensionHostProcessOptions): Promise<{ pid: number; }>;
|
||||
createExtensionHost(): Promise<{ id: string }>;
|
||||
start(id: string, opts: IExtensionHostProcessOptions): Promise<{ pid: number }>;
|
||||
enableInspectPort(id: string): Promise<boolean>;
|
||||
kill(id: string): Promise<void>;
|
||||
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isEqualOrParent, joinPath } from 'vs/base/common/resources';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as semver from 'vs/base/common/semver/semver';
|
||||
import { IExtensionManifest } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
export interface IParsedVersion {
|
||||
hasCaret: boolean;
|
||||
@@ -232,27 +237,109 @@ export function isValidVersion(_inputVersion: string | INormalizedVersion, _inpu
|
||||
return true;
|
||||
}
|
||||
|
||||
export interface IReducedExtensionDescription {
|
||||
isBuiltin: boolean;
|
||||
engines: {
|
||||
vscode: string;
|
||||
// {{SQL CARBON EDIT}}
|
||||
azdata?: string;
|
||||
};
|
||||
main?: string;
|
||||
}
|
||||
|
||||
type ProductDate = string | Date | undefined;
|
||||
|
||||
export function isValidExtensionVersion(version: string, date: ProductDate, extensionDesc: IReducedExtensionDescription, notices: string[]): boolean {
|
||||
export function validateExtensionManifest(productVersion: string, productDate: ProductDate, extensionLocation: URI, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean): readonly [Severity, string][] {
|
||||
const validations: [Severity, string][] = [];
|
||||
if (typeof extensionManifest.publisher !== 'undefined' && typeof extensionManifest.publisher !== 'string') {
|
||||
validations.push([Severity.Error, nls.localize('extensionDescription.publisher', "property publisher must be of type `string`.")]);
|
||||
return validations;
|
||||
}
|
||||
if (typeof extensionManifest.name !== 'string') {
|
||||
validations.push([Severity.Error, nls.localize('extensionDescription.name', "property `{0}` is mandatory and must be of type `string`", 'name')]);
|
||||
return validations;
|
||||
}
|
||||
if (typeof extensionManifest.version !== 'string') {
|
||||
validations.push([Severity.Error, nls.localize('extensionDescription.version', "property `{0}` is mandatory and must be of type `string`", 'version')]);
|
||||
return validations;
|
||||
}
|
||||
if (!extensionManifest.engines) {
|
||||
validations.push([Severity.Error, nls.localize('extensionDescription.engines', "property `{0}` is mandatory and must be of type `object`", 'engines')]);
|
||||
return validations;
|
||||
}
|
||||
if (typeof extensionManifest.engines.vscode !== 'string') {
|
||||
validations.push([Severity.Error, nls.localize('extensionDescription.engines.vscode', "property `{0}` is mandatory and must be of type `string`", 'engines.vscode')]);
|
||||
return validations;
|
||||
}
|
||||
if (typeof extensionManifest.extensionDependencies !== 'undefined') {
|
||||
if (!isStringArray(extensionManifest.extensionDependencies)) {
|
||||
validations.push([Severity.Error, nls.localize('extensionDescription.extensionDependencies', "property `{0}` can be omitted or must be of type `string[]`", 'extensionDependencies')]);
|
||||
return validations;
|
||||
}
|
||||
}
|
||||
if (typeof extensionManifest.activationEvents !== 'undefined') {
|
||||
if (!isStringArray(extensionManifest.activationEvents)) {
|
||||
validations.push([Severity.Error, nls.localize('extensionDescription.activationEvents1', "property `{0}` can be omitted or must be of type `string[]`", 'activationEvents')]);
|
||||
return validations;
|
||||
}
|
||||
if (typeof extensionManifest.main === 'undefined' && typeof extensionManifest.browser === 'undefined') {
|
||||
validations.push([Severity.Error, nls.localize('extensionDescription.activationEvents2', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'main')]);
|
||||
return validations;
|
||||
}
|
||||
}
|
||||
if (typeof extensionManifest.extensionKind !== 'undefined') {
|
||||
if (typeof extensionManifest.main === 'undefined') {
|
||||
validations.push([Severity.Warning, nls.localize('extensionDescription.extensionKind', "property `{0}` can be defined only if property `main` is also defined.", 'extensionKind')]);
|
||||
// not a failure case
|
||||
}
|
||||
}
|
||||
if (typeof extensionManifest.main !== 'undefined') {
|
||||
if (typeof extensionManifest.main !== 'string') {
|
||||
validations.push([Severity.Error, nls.localize('extensionDescription.main1', "property `{0}` can be omitted or must be of type `string`", 'main')]);
|
||||
return validations;
|
||||
} else {
|
||||
const mainLocation = joinPath(extensionLocation, extensionManifest.main);
|
||||
if (!isEqualOrParent(mainLocation, extensionLocation)) {
|
||||
validations.push([Severity.Warning, nls.localize('extensionDescription.main2', "Expected `main` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", mainLocation.path, extensionLocation.path)]);
|
||||
// not a failure case
|
||||
}
|
||||
}
|
||||
if (typeof extensionManifest.activationEvents === 'undefined') {
|
||||
validations.push([Severity.Error, nls.localize('extensionDescription.main3', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'main')]);
|
||||
return validations;
|
||||
}
|
||||
}
|
||||
if (typeof extensionManifest.browser !== 'undefined') {
|
||||
if (typeof extensionManifest.browser !== 'string') {
|
||||
validations.push([Severity.Error, nls.localize('extensionDescription.browser1', "property `{0}` can be omitted or must be of type `string`", 'browser')]);
|
||||
return validations;
|
||||
} else {
|
||||
const browserLocation = joinPath(extensionLocation, extensionManifest.browser);
|
||||
if (!isEqualOrParent(browserLocation, extensionLocation)) {
|
||||
validations.push([Severity.Warning, nls.localize('extensionDescription.browser2', "Expected `browser` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", browserLocation.path, extensionLocation.path)]);
|
||||
// not a failure case
|
||||
}
|
||||
}
|
||||
if (typeof extensionManifest.activationEvents === 'undefined') {
|
||||
validations.push([Severity.Error, nls.localize('extensionDescription.browser3', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'browser')]);
|
||||
return validations;
|
||||
}
|
||||
}
|
||||
|
||||
if (extensionDesc.isBuiltin || typeof extensionDesc.main === 'undefined') {
|
||||
if (!semver.valid(extensionManifest.version)) {
|
||||
validations.push([Severity.Error, nls.localize('notSemver', "Extension version is not semver compatible.")]);
|
||||
return validations;
|
||||
}
|
||||
|
||||
const notices: string[] = [];
|
||||
const isValid = isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices);
|
||||
if (!isValid) {
|
||||
for (const notice of notices) {
|
||||
validations.push([Severity.Error, notice]);
|
||||
}
|
||||
}
|
||||
return validations;
|
||||
}
|
||||
|
||||
export function isValidExtensionVersion(productVersion: string, productDate: ProductDate, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean, notices: string[]): boolean {
|
||||
|
||||
if (extensionIsBuiltin || (typeof extensionManifest.main === 'undefined' && typeof extensionManifest.browser === 'undefined')) {
|
||||
// No version check for builtin or declarative extensions
|
||||
return true;
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
return extensionDesc.engines.azdata ? extensionDesc.engines.azdata === '*' || isVersionValid(version, date, extensionDesc.engines.azdata, notices) : true;
|
||||
return extensionManifest.engines.azdata ? extensionManifest.engines.azdata === '*' || isVersionValid(productVersion, productDate, extensionManifest.engines.vscode, notices) : true;
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
@@ -293,3 +380,15 @@ function isVersionValid(currentVersion: string, date: ProductDate, requestedVers
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isStringArray(arr: string[]): boolean {
|
||||
if (!Array.isArray(arr)) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0, len = arr.length; i < len; i++) {
|
||||
if (typeof arr[i] !== 'string') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3,16 +3,17 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ExtensionKind } from 'vs/platform/environment/common/environment';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ILocalization } from 'vs/platform/localizations/common/localizations';
|
||||
import { getRemoteName } from 'vs/platform/remote/common/remoteHosts';
|
||||
|
||||
export const MANIFEST_CACHE_FOLDER = 'CachedExtensions';
|
||||
export const USER_MANIFEST_CACHE_FILE = 'user';
|
||||
export const BUILTIN_MANIFEST_CACHE_FILE = 'builtin';
|
||||
|
||||
export const UNDEFINED_PUBLISHER = 'undefined_publisher';
|
||||
export const ExtensionsPolicyKey = 'extensions.extensionsPolicy'; // {{SQL CARBON EDIT}} start
|
||||
export enum ExtensionsPolicy {
|
||||
allowAll = 'allowAll',
|
||||
@@ -33,10 +34,10 @@ export interface IConfigurationProperty {
|
||||
}
|
||||
|
||||
export interface IConfiguration {
|
||||
id?: string,
|
||||
order?: number,
|
||||
title?: string,
|
||||
properties: { [key: string]: IConfigurationProperty; };
|
||||
id?: string;
|
||||
order?: number;
|
||||
title?: string;
|
||||
properties: { [key: string]: IConfigurationProperty };
|
||||
}
|
||||
|
||||
export interface IDebugger {
|
||||
@@ -97,7 +98,7 @@ export interface IView {
|
||||
export interface IColor {
|
||||
id: string;
|
||||
description: string;
|
||||
defaults: { light: string, dark: string, highContrast: string };
|
||||
defaults: { light: string; dark: string; highContrast: string };
|
||||
}
|
||||
|
||||
export interface IWebviewEditor {
|
||||
@@ -129,9 +130,9 @@ export interface IWalkthroughStep {
|
||||
readonly title: string;
|
||||
readonly description: string | undefined;
|
||||
readonly media:
|
||||
| { image: string | { dark: string, light: string, hc: string }, altText: string, markdown?: never, svg?: never }
|
||||
| { markdown: string, image?: never, svg?: never }
|
||||
| { svg: string, altText: string, markdown?: never, image?: never }
|
||||
| { image: string | { dark: string; light: string; hc: string }; altText: string; markdown?: never; svg?: never }
|
||||
| { markdown: string; image?: never; svg?: never }
|
||||
| { svg: string; altText: string; markdown?: never; image?: never };
|
||||
readonly completionEvents?: string[];
|
||||
/** @deprecated use `completionEvents: 'onCommand:...'` */
|
||||
readonly doneOn?: { command: string };
|
||||
@@ -139,7 +140,7 @@ export interface IWalkthroughStep {
|
||||
}
|
||||
|
||||
export interface IWalkthrough {
|
||||
readonly id: string,
|
||||
readonly id: string;
|
||||
readonly title: string;
|
||||
readonly description: string;
|
||||
readonly steps: IWalkthroughStep[];
|
||||
@@ -155,8 +156,28 @@ export interface IStartEntry {
|
||||
readonly category: 'file' | 'folder' | 'notebook';
|
||||
}
|
||||
|
||||
export interface INotebookEntry {
|
||||
readonly type: string;
|
||||
readonly displayName: string;
|
||||
}
|
||||
|
||||
export interface INotebookRendererContribution {
|
||||
readonly id: string;
|
||||
readonly displayName: string;
|
||||
readonly mimeTypes: string[];
|
||||
}
|
||||
|
||||
export interface ITranslation {
|
||||
id: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface ILocalizationContribution {
|
||||
languageId: string;
|
||||
languageName?: string;
|
||||
localizedLanguageName?: string;
|
||||
translations: ITranslation[];
|
||||
minimalTranslations?: { [key: string]: string };
|
||||
}
|
||||
|
||||
export interface IExtensionContributions {
|
||||
@@ -175,12 +196,13 @@ export interface IExtensionContributions {
|
||||
viewsContainers?: { [location: string]: IViewContainer[] };
|
||||
views?: { [location: string]: IView[] };
|
||||
colors?: IColor[];
|
||||
localizations?: ILocalization[];
|
||||
localizations?: ILocalizationContribution[];
|
||||
readonly customEditors?: readonly IWebviewEditor[];
|
||||
readonly codeActions?: readonly ICodeActionContribution[];
|
||||
authentication?: IAuthenticationContribution[];
|
||||
walkthroughs?: IWalkthrough[];
|
||||
startEntries?: IStartEntry[];
|
||||
readonly notebooks?: INotebookEntry[];
|
||||
readonly notebookRenderer?: INotebookRendererContribution[];
|
||||
}
|
||||
|
||||
@@ -191,14 +213,13 @@ export interface IExtensionCapabilities {
|
||||
|
||||
|
||||
export const ALL_EXTENSION_KINDS: readonly ExtensionKind[] = ['ui', 'workspace', 'web'];
|
||||
export type ExtensionKind = 'ui' | 'workspace' | 'web';
|
||||
|
||||
export type LimitedWorkspaceSupportType = 'limited';
|
||||
export type ExtensionUntrustedWorkspaceSupportType = boolean | LimitedWorkspaceSupportType;
|
||||
export type ExtensionUntrustedWorkspaceSupport = { supported: true; } | { supported: false, description: string } | { supported: LimitedWorkspaceSupportType, description: string, restrictedConfigurations?: string[] };
|
||||
export type ExtensionUntrustedWorkspaceSupport = { supported: true } | { supported: false; description: string } | { supported: LimitedWorkspaceSupportType; description: string; restrictedConfigurations?: string[] };
|
||||
|
||||
export type ExtensionVirtualWorkspaceSupportType = boolean | LimitedWorkspaceSupportType;
|
||||
export type ExtensionVirtualWorkspaceSupport = boolean | { supported: true; } | { supported: false | LimitedWorkspaceSupportType, description: string };
|
||||
export type ExtensionVirtualWorkspaceSupport = boolean | { supported: true } | { supported: false | LimitedWorkspaceSupportType; description: string };
|
||||
|
||||
export function getWorkspaceSupportTypeMessage(supportType: ExtensionUntrustedWorkspaceSupport | ExtensionVirtualWorkspaceSupport | undefined): string | undefined {
|
||||
if (typeof supportType === 'object' && supportType !== null) {
|
||||
@@ -244,45 +265,72 @@ export const EXTENSION_CATEGORIES = [
|
||||
// 'Other',
|
||||
];
|
||||
|
||||
export interface IExtensionManifest {
|
||||
readonly name: string;
|
||||
readonly displayName?: string;
|
||||
readonly publisher: string;
|
||||
readonly version: string;
|
||||
readonly engines: { vscode: string; azdata?: string }; // {{SQL CARBON EDIT}} add field
|
||||
readonly forceReload?: boolean; // {{SQL CARBON EDIT}} add field
|
||||
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; };
|
||||
readonly capabilities?: IExtensionCapabilities;
|
||||
export interface IRelaxedExtensionManifest {
|
||||
name: string;
|
||||
displayName?: string;
|
||||
publisher: string;
|
||||
version: string;
|
||||
engines: { vscode: string; azdata?: string }; // {{SQL CARBON EDIT}} add field
|
||||
forceReload?: boolean; // {{SQL CARBON EDIT}} add field
|
||||
description?: string;
|
||||
main?: string;
|
||||
browser?: string;
|
||||
icon?: string;
|
||||
categories?: string[];
|
||||
keywords?: string[];
|
||||
activationEvents?: string[];
|
||||
extensionDependencies?: string[];
|
||||
extensionPack?: string[];
|
||||
extensionKind?: ExtensionKind | ExtensionKind[];
|
||||
contributes?: IExtensionContributions;
|
||||
repository?: { url: string };
|
||||
bugs?: { url: string };
|
||||
enabledApiProposals?: readonly string[];
|
||||
api?: string;
|
||||
scripts?: { [key: string]: string };
|
||||
capabilities?: IExtensionCapabilities;
|
||||
}
|
||||
|
||||
export type IExtensionManifest = Readonly<IRelaxedExtensionManifest>;
|
||||
|
||||
export const enum ExtensionType {
|
||||
System,
|
||||
User
|
||||
}
|
||||
|
||||
export const enum TargetPlatform {
|
||||
WIN32_X64 = 'win32-x64',
|
||||
WIN32_IA32 = 'win32-ia32',
|
||||
WIN32_ARM64 = 'win32-arm64',
|
||||
|
||||
LINUX_X64 = 'linux-x64',
|
||||
LINUX_ARM64 = 'linux-arm64',
|
||||
LINUX_ARMHF = 'linux-armhf',
|
||||
|
||||
ALPINE_X64 = 'alpine-x64',
|
||||
ALPINE_ARM64 = 'alpine-arm64',
|
||||
|
||||
DARWIN_X64 = 'darwin-x64',
|
||||
DARWIN_ARM64 = 'darwin-arm64',
|
||||
|
||||
WEB = 'web',
|
||||
|
||||
UNIVERSAL = 'universal',
|
||||
UNKNOWN = 'unknown',
|
||||
UNDEFINED = 'undefined',
|
||||
}
|
||||
|
||||
export interface IExtension {
|
||||
readonly type: ExtensionType;
|
||||
readonly isBuiltin: boolean;
|
||||
readonly identifier: IExtensionIdentifier;
|
||||
readonly manifest: IExtensionManifest;
|
||||
readonly location: URI;
|
||||
readonly targetPlatform: TargetPlatform;
|
||||
readonly readmeUrl?: URI;
|
||||
readonly changelogUrl?: URI;
|
||||
readonly isValid: boolean;
|
||||
readonly validations: readonly [Severity, string][];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -340,17 +388,20 @@ export class ExtensionIdentifier {
|
||||
}
|
||||
}
|
||||
|
||||
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 interface IRelaxedExtensionDescription extends IRelaxedExtensionManifest {
|
||||
id?: string;
|
||||
identifier: ExtensionIdentifier;
|
||||
uuid?: string;
|
||||
targetPlatform: TargetPlatform;
|
||||
isBuiltin: boolean;
|
||||
isUserBuiltin: boolean;
|
||||
isUnderDevelopment: boolean;
|
||||
extensionLocation: URI;
|
||||
readonly forceReload?: boolean; // {{SQL CARBON EDIT}}
|
||||
}
|
||||
|
||||
export type IExtensionDescription = Readonly<IRelaxedExtensionDescription>;
|
||||
|
||||
export function isLanguagePackExtension(manifest: IExtensionManifest): boolean {
|
||||
return manifest.contributes && manifest.contributes.localizations ? manifest.contributes.localizations.length > 0 : false;
|
||||
}
|
||||
@@ -360,7 +411,7 @@ export function isAuthenticationProviderExtension(manifest: IExtensionManifest):
|
||||
}
|
||||
|
||||
export function isResolverExtension(manifest: IExtensionManifest, remoteAuthority: string | undefined): boolean {
|
||||
if (remoteAuthority && manifest.enableProposedApi) {
|
||||
if (remoteAuthority) {
|
||||
const activationEvent = `onResolveRemoteAuthority:${getRemoteName(remoteAuthority)}`;
|
||||
return manifest.activationEvents?.indexOf(activationEvent) !== -1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { canceled, SerializedError } from 'vs/base/common/errors';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IExtensionHostProcessOptions, IExtensionHostStarter } from 'vs/platform/extensions/common/extensionHostStarter';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { FileAccess } from 'vs/base/common/network';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { Worker } from 'worker_threads';
|
||||
import { IWorker, IWorkerCallback, IWorkerFactory, SimpleWorkerClient } from 'vs/base/common/worker/simpleWorker';
|
||||
import type { ExtensionHostStarter, IExtensionHostStarterWorkerHost } from 'vs/platform/extensions/node/extensionHostStarterWorker';
|
||||
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
|
||||
import { StopWatch } from 'vs/base/common/stopwatch';
|
||||
|
||||
class NodeWorker implements IWorker {
|
||||
|
||||
private readonly _worker: Worker;
|
||||
|
||||
public readonly onError: Event<Error>;
|
||||
public readonly onExit: Event<number>;
|
||||
public readonly onMessageError: Event<Error>;
|
||||
|
||||
constructor(callback: IWorkerCallback, onErrorCallback: (err: any) => void) {
|
||||
this._worker = new Worker(
|
||||
FileAccess.asFileUri('vs/platform/extensions/node/extensionHostStarterWorkerMain.js', require).fsPath,
|
||||
);
|
||||
this._worker.on('message', callback);
|
||||
this._worker.on('error', onErrorCallback);
|
||||
this.onError = Event.fromNodeEventEmitter(this._worker, 'error');
|
||||
this.onExit = Event.fromNodeEventEmitter(this._worker, 'exit');
|
||||
this.onMessageError = Event.fromNodeEventEmitter(this._worker, 'messageerror');
|
||||
}
|
||||
|
||||
getId(): number {
|
||||
return 1;
|
||||
}
|
||||
|
||||
postMessage(message: any, transfer: ArrayBuffer[]): void {
|
||||
this._worker.postMessage(message, transfer);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._worker.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
class ExtensionHostStarterWorkerHost implements IExtensionHostStarterWorkerHost {
|
||||
constructor(
|
||||
@ILogService private readonly _logService: ILogService
|
||||
) { }
|
||||
|
||||
public async logInfo(message: string): Promise<void> {
|
||||
this._logService.info(message);
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkerMainProcessExtensionHostStarter implements IDisposable, IExtensionHostStarter {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private _proxy: ExtensionHostStarter | null;
|
||||
private readonly _worker: SimpleWorkerClient<ExtensionHostStarter, IExtensionHostStarterWorkerHost>;
|
||||
private _shutdown = false;
|
||||
|
||||
constructor(
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@ILifecycleMainService lifecycleMainService: ILifecycleMainService
|
||||
) {
|
||||
this._proxy = null;
|
||||
|
||||
const workerFactory: IWorkerFactory = {
|
||||
create: (moduleId: string, callback: IWorkerCallback, onErrorCallback: (err: any) => void): IWorker => {
|
||||
const worker = new NodeWorker(callback, onErrorCallback);
|
||||
worker.onError((err) => {
|
||||
this._logService.error(`ExtensionHostStarterWorker has encountered an error:`);
|
||||
this._logService.error(err);
|
||||
});
|
||||
worker.onMessageError((err) => {
|
||||
this._logService.error(`ExtensionHostStarterWorker has encountered a message error:`);
|
||||
this._logService.error(err);
|
||||
});
|
||||
worker.onExit((exitCode) => this._logService.info(`ExtensionHostStarterWorker exited with code ${exitCode}.`));
|
||||
worker.postMessage(moduleId, []);
|
||||
return worker;
|
||||
}
|
||||
};
|
||||
this._worker = new SimpleWorkerClient<ExtensionHostStarter, IExtensionHostStarterWorkerHost>(
|
||||
workerFactory,
|
||||
'vs/platform/extensions/node/extensionHostStarterWorker',
|
||||
new ExtensionHostStarterWorkerHost(this._logService)
|
||||
);
|
||||
this._initialize();
|
||||
|
||||
// On shutdown: gracefully await extension host shutdowns
|
||||
lifecycleMainService.onWillShutdown((e) => {
|
||||
this._shutdown = true;
|
||||
if (this._proxy) {
|
||||
e.join(this._proxy.waitForAllExit(6000));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// Intentionally not killing the extension host processes
|
||||
}
|
||||
|
||||
async _initialize(): Promise<void> {
|
||||
this._proxy = await this._worker.getProxyObject();
|
||||
this._logService.info(`ExtensionHostStarterWorker created`);
|
||||
}
|
||||
|
||||
onDynamicStdout(id: string): Event<string> {
|
||||
return this._proxy!.onDynamicStdout(id);
|
||||
}
|
||||
|
||||
onDynamicStderr(id: string): Event<string> {
|
||||
return this._proxy!.onDynamicStderr(id);
|
||||
}
|
||||
|
||||
onDynamicMessage(id: string): Event<any> {
|
||||
return this._proxy!.onDynamicMessage(id);
|
||||
}
|
||||
|
||||
onDynamicError(id: string): Event<{ error: SerializedError }> {
|
||||
return this._proxy!.onDynamicError(id);
|
||||
}
|
||||
|
||||
onDynamicExit(id: string): Event<{ code: number; signal: string }> {
|
||||
return this._proxy!.onDynamicExit(id);
|
||||
}
|
||||
|
||||
async createExtensionHost(): Promise<{ id: string }> {
|
||||
const proxy = await this._worker.getProxyObject();
|
||||
if (this._shutdown) {
|
||||
throw canceled();
|
||||
}
|
||||
return proxy.createExtensionHost();
|
||||
}
|
||||
|
||||
async start(id: string, opts: IExtensionHostProcessOptions): Promise<{ pid: number }> {
|
||||
const sw = StopWatch.create(false);
|
||||
const proxy = await this._worker.getProxyObject();
|
||||
if (this._shutdown) {
|
||||
throw canceled();
|
||||
}
|
||||
const timeout = setTimeout(() => {
|
||||
this._logService.info(`ExtensionHostStarterWorker.start() did not return within 30s. This might be a problem.`);
|
||||
}, 30000);
|
||||
const result = await proxy.start(id, opts);
|
||||
const duration = sw.elapsed();
|
||||
this._logService.info(`ExtensionHostStarterWorker.start() took ${duration} ms.`);
|
||||
clearTimeout(timeout);
|
||||
return result;
|
||||
}
|
||||
|
||||
async enableInspectPort(id: string): Promise<boolean> {
|
||||
const proxy = await this._worker.getProxyObject();
|
||||
if (this._shutdown) {
|
||||
throw canceled();
|
||||
}
|
||||
return proxy.enableInspectPort(id);
|
||||
}
|
||||
|
||||
async kill(id: string): Promise<void> {
|
||||
const proxy = await this._worker.getProxyObject();
|
||||
if (this._shutdown) {
|
||||
throw canceled();
|
||||
}
|
||||
return proxy.kill(id);
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,22 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SerializedError, transformErrorForSerialization } from 'vs/base/common/errors';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IExtensionHostProcessOptions, IExtensionHostStarter } from 'vs/platform/extensions/common/extensionHostStarter';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { ChildProcess, fork } from 'child_process';
|
||||
import { FileAccess } from 'vs/base/common/network';
|
||||
import { StringDecoder } from 'string_decoder';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { Promises, timeout } from 'vs/base/common/async';
|
||||
import { SerializedError, transformErrorForSerialization } from 'vs/base/common/errors';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { FileAccess } from 'vs/base/common/network';
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { cwd } from 'vs/base/common/process';
|
||||
import { StopWatch } from 'vs/base/common/stopwatch';
|
||||
import { IExtensionHostProcessOptions, IExtensionHostStarter } from 'vs/platform/extensions/common/extensionHostStarter';
|
||||
|
||||
export interface IExtensionHostStarterWorkerHost {
|
||||
logInfo(message: string): Promise<void>;
|
||||
}
|
||||
|
||||
class ExtensionHostProcess extends Disposable {
|
||||
|
||||
@@ -27,34 +31,36 @@ class ExtensionHostProcess extends Disposable {
|
||||
readonly _onMessage = this._register(new Emitter<any>());
|
||||
readonly onMessage = this._onMessage.event;
|
||||
|
||||
readonly _onError = this._register(new Emitter<{ error: SerializedError; }>());
|
||||
readonly _onError = this._register(new Emitter<{ error: SerializedError }>());
|
||||
readonly onError = this._onError.event;
|
||||
|
||||
readonly _onExit = this._register(new Emitter<{ pid: number; code: number; signal: string }>());
|
||||
readonly onExit = this._onExit.event;
|
||||
|
||||
private _process: ChildProcess | null = null;
|
||||
private _hasExited: boolean = false;
|
||||
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
@ILogService private readonly _logService: ILogService
|
||||
private readonly _host: IExtensionHostStarterWorkerHost
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
register(disposable: IDisposable) {
|
||||
this._register(disposable);
|
||||
}
|
||||
|
||||
start(opts: IExtensionHostProcessOptions): { pid: number; } {
|
||||
start(opts: IExtensionHostProcessOptions): { pid: number } {
|
||||
if (platform.isCI) {
|
||||
this._host.logInfo(`Calling fork to start extension host...`);
|
||||
}
|
||||
const sw = StopWatch.create(false);
|
||||
this._process = fork(
|
||||
FileAccess.asFileUri('bootstrap-fork', require).fsPath,
|
||||
['--type=extensionHost', '--skipWorkspaceStorageLock'],
|
||||
mixin({ cwd: cwd() }, opts),
|
||||
);
|
||||
const pid = this._process.pid;
|
||||
const forkTime = sw.elapsed();
|
||||
const pid = this._process.pid!;
|
||||
|
||||
this._logService.info(`Starting extension host with pid ${pid}.`);
|
||||
this._host.logInfo(`Starting extension host with pid ${pid} (fork() took ${forkTime} ms).`);
|
||||
|
||||
const stdoutDecoder = new StringDecoder('utf-8');
|
||||
this._process.stdout?.on('data', (chunk) => {
|
||||
@@ -77,6 +83,7 @@ class ExtensionHostProcess extends Disposable {
|
||||
});
|
||||
|
||||
this._process.on('exit', (code: number, signal: string) => {
|
||||
this._hasExited = true;
|
||||
this._onExit.fire({ pid, code, signal });
|
||||
});
|
||||
|
||||
@@ -88,7 +95,7 @@ class ExtensionHostProcess extends Disposable {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._logService.info(`Enabling inspect port on extension host with pid ${this._process.pid}.`);
|
||||
this._host.logInfo(`Enabling inspect port on extension host with pid ${this._process.pid}.`);
|
||||
|
||||
interface ProcessExt {
|
||||
_debugProcess?(n: number): any;
|
||||
@@ -96,7 +103,7 @@ class ExtensionHostProcess extends Disposable {
|
||||
|
||||
if (typeof (<ProcessExt>process)._debugProcess === 'function') {
|
||||
// use (undocumented) _debugProcess feature of node
|
||||
(<ProcessExt>process)._debugProcess!(this._process.pid);
|
||||
(<ProcessExt>process)._debugProcess!(this._process.pid!);
|
||||
return true;
|
||||
} else if (!platform.isWindows) {
|
||||
// use KILL USR1 on non-windows platforms (fallback)
|
||||
@@ -112,9 +119,24 @@ class ExtensionHostProcess extends Disposable {
|
||||
if (!this._process) {
|
||||
return;
|
||||
}
|
||||
this._logService.info(`Killing extension host with pid ${this._process.pid}.`);
|
||||
this._host.logInfo(`Killing extension host with pid ${this._process.pid}.`);
|
||||
this._process.kill();
|
||||
}
|
||||
|
||||
async waitForExit(maxWaitTimeMs: number): Promise<void> {
|
||||
if (!this._process) {
|
||||
return;
|
||||
}
|
||||
const pid = this._process.pid;
|
||||
this._host.logInfo(`Waiting for extension host with pid ${pid} to exit.`);
|
||||
await Promise.race([Event.toPromise(this.onExit), timeout(maxWaitTimeMs)]);
|
||||
|
||||
if (!this._hasExited) {
|
||||
// looks like we timed out
|
||||
this._host.logInfo(`Extension host with pid ${pid} did not exit within ${maxWaitTimeMs}ms.`);
|
||||
this._process.kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter {
|
||||
@@ -122,10 +144,10 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter
|
||||
|
||||
private static _lastId: number = 0;
|
||||
|
||||
private readonly _extHosts: Map<string, ExtensionHostProcess>;
|
||||
protected readonly _extHosts: Map<string, ExtensionHostProcess>;
|
||||
|
||||
constructor(
|
||||
@ILogService private readonly _logService: ILogService
|
||||
private readonly _host: IExtensionHostStarterWorkerHost
|
||||
) {
|
||||
this._extHosts = new Map<string, ExtensionHostProcess>();
|
||||
}
|
||||
@@ -154,20 +176,20 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter
|
||||
return this._getExtHost(id).onMessage;
|
||||
}
|
||||
|
||||
onDynamicError(id: string): Event<{ error: SerializedError; }> {
|
||||
onDynamicError(id: string): Event<{ error: SerializedError }> {
|
||||
return this._getExtHost(id).onError;
|
||||
}
|
||||
|
||||
onDynamicExit(id: string): Event<{ code: number; signal: string; }> {
|
||||
onDynamicExit(id: string): Event<{ code: number; signal: string }> {
|
||||
return this._getExtHost(id).onExit;
|
||||
}
|
||||
|
||||
async createExtensionHost(): Promise<{ id: string; }> {
|
||||
async createExtensionHost(): Promise<{ id: string }> {
|
||||
const id = String(++ExtensionHostStarter._lastId);
|
||||
const extHost = new ExtensionHostProcess(id, this._logService);
|
||||
const extHost = new ExtensionHostProcess(id, this._host);
|
||||
this._extHosts.set(id, extHost);
|
||||
extHost.onExit(({ pid, code, signal }) => {
|
||||
this._logService.info(`Extension host with pid ${pid} exited with code: ${code}, signal: ${signal}.`);
|
||||
this._host.logInfo(`Extension host with pid ${pid} exited with code: ${code}, signal: ${signal}.`);
|
||||
setTimeout(() => {
|
||||
extHost.dispose();
|
||||
this._extHosts.delete(id);
|
||||
@@ -176,7 +198,7 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter
|
||||
return { id };
|
||||
}
|
||||
|
||||
async start(id: string, opts: IExtensionHostProcessOptions): Promise<{ pid: number; }> {
|
||||
async start(id: string, opts: IExtensionHostProcessOptions): Promise<{ pid: number }> {
|
||||
return this._getExtHost(id).start(opts);
|
||||
}
|
||||
|
||||
@@ -196,6 +218,26 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter
|
||||
}
|
||||
extHostProcess.kill();
|
||||
}
|
||||
|
||||
async killAllNow(): Promise<void> {
|
||||
for (const [, extHost] of this._extHosts) {
|
||||
extHost.kill();
|
||||
}
|
||||
}
|
||||
|
||||
async waitForAllExit(maxWaitTimeMs: number): Promise<void> {
|
||||
const exitPromises: Promise<void>[] = [];
|
||||
for (const [, extHost] of this._extHosts) {
|
||||
exitPromises.push(extHost.waitForExit(maxWaitTimeMs));
|
||||
}
|
||||
return Promises.settled(exitPromises).then(() => { });
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IExtensionHostStarter, ExtensionHostStarter, true);
|
||||
/**
|
||||
* The `create` function needs to be there by convention because
|
||||
* we are loaded via the `vs/base/common/worker/simpleWorker` utility.
|
||||
*/
|
||||
export function create(host: IExtensionHostStarterWorkerHost) {
|
||||
return new ExtensionHostStarter(host);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const loader = require('../../../loader');
|
||||
const bootstrap = require('../../../../bootstrap');
|
||||
const path = require('path');
|
||||
const parentPort = require('worker_threads').parentPort;
|
||||
|
||||
// Bootstrap: NLS
|
||||
const nlsConfig = bootstrap.setupNLS();
|
||||
|
||||
// Bootstrap: Loader
|
||||
loader.config({
|
||||
baseUrl: bootstrap.fileUriFromPath(path.join(__dirname, '../../../../'), { isWindows: process.platform === 'win32' }),
|
||||
catchError: true,
|
||||
nodeRequire: require,
|
||||
nodeMain: __filename,
|
||||
'vs/nls': nlsConfig,
|
||||
amdModulesPattern: /^vs\//,
|
||||
recordStats: true
|
||||
});
|
||||
|
||||
let isFirstMessage = true;
|
||||
let beforeReadyMessages: any[] = [];
|
||||
|
||||
const initialMessageHandler = (data: any) => {
|
||||
if (!isFirstMessage) {
|
||||
beforeReadyMessages.push(data);
|
||||
return;
|
||||
}
|
||||
|
||||
isFirstMessage = false;
|
||||
loadCode(data);
|
||||
};
|
||||
|
||||
parentPort.on('message', initialMessageHandler);
|
||||
|
||||
const loadCode = function (moduleId: string) {
|
||||
loader([moduleId], function (ws: any) {
|
||||
setTimeout(() => {
|
||||
|
||||
const messageHandler = ws.create((msg: any, transfer?: ArrayBuffer[]) => {
|
||||
parentPort.postMessage(msg, transfer);
|
||||
}, null);
|
||||
parentPort.off('message', initialMessageHandler);
|
||||
parentPort.on('message', (data: any) => {
|
||||
messageHandler.onmessage(data);
|
||||
});
|
||||
while (beforeReadyMessages.length > 0) {
|
||||
const msg = beforeReadyMessages.shift()!;
|
||||
messageHandler.onmessage(msg);
|
||||
}
|
||||
|
||||
});
|
||||
}, (err: any) => console.error(err));
|
||||
};
|
||||
|
||||
parentPort.on('messageerror', (err: Error) => {
|
||||
console.error(err);
|
||||
});
|
||||
})();
|
||||
@@ -3,7 +3,8 @@
|
||||
* Licensed under the Source EULA. 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';
|
||||
import { IExtensionManifest } from 'vs/platform/extensions/common/extensions';
|
||||
import { INormalizedVersion, IParsedVersion, isValidExtensionVersion, isValidVersion, isValidVersionStr, normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator';
|
||||
|
||||
suite('Extension Version Validator', () => {
|
||||
const productVersion = '2021-05-11T21:54:30.577Z';
|
||||
@@ -208,17 +209,19 @@ suite('Extension Version Validator', () => {
|
||||
test.skip('isValidExtensionVersion', () => { // {{SQL CARBON EDIT}} skip test
|
||||
|
||||
function testExtensionVersion(version: string, desiredVersion: string, isBuiltin: boolean, hasMain: boolean, expectedResult: boolean): void {
|
||||
let desc: IReducedExtensionDescription = {
|
||||
isBuiltin: isBuiltin,
|
||||
const manifest: IExtensionManifest = {
|
||||
name: 'test',
|
||||
publisher: 'test',
|
||||
version: '0.0.0',
|
||||
engines: {
|
||||
vscode: desiredVersion
|
||||
},
|
||||
main: hasMain ? 'something' : undefined
|
||||
};
|
||||
let reasons: string[] = [];
|
||||
let actual = isValidExtensionVersion(version, productVersion, desc, reasons);
|
||||
let actual = isValidExtensionVersion(version, productVersion, manifest, isBuiltin, reasons);
|
||||
|
||||
assert.strictEqual(actual, expectedResult, 'version: ' + version + ', desiredVersion: ' + desiredVersion + ', desc: ' + JSON.stringify(desc) + ', reasons: ' + JSON.stringify(reasons));
|
||||
assert.strictEqual(actual, expectedResult, 'version: ' + version + ', desiredVersion: ' + desiredVersion + ', desc: ' + JSON.stringify(manifest) + ', reasons: ' + JSON.stringify(reasons));
|
||||
}
|
||||
|
||||
function testIsInvalidExtensionVersion(version: string, desiredVersion: string, isBuiltin: boolean, hasMain: boolean): void {
|
||||
@@ -403,4 +406,17 @@ suite('Extension Version Validator', () => {
|
||||
testIsValidVersion('1.10.1', '^1.10.0-20200101', true); // before date, but ahead version
|
||||
testIsValidVersion('1.11.0', '^1.10.0-20200101', true);
|
||||
});
|
||||
|
||||
test.skip('isValidExtensionVersion checks browser only extensions', () => {
|
||||
const manifest = {
|
||||
name: 'test',
|
||||
publisher: 'test',
|
||||
version: '0.0.0',
|
||||
engines: {
|
||||
vscode: '^1.45.0'
|
||||
},
|
||||
browser: 'something'
|
||||
};
|
||||
assert.strictEqual(isValidExtensionVersion('1.44.0', undefined, manifest, false, []), false);
|
||||
});
|
||||
});
|
||||
|
||||
26
src/vs/platform/externalServices/common/marketplace.ts
Normal file
26
src/vs/platform/externalServices/common/marketplace.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IHeaders } from 'vs/base/parts/request/common/request';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { getServiceMachineId } from 'vs/platform/externalServices/common/serviceMachineId';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { getTelemetryLevel, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
|
||||
export async function resolveMarketplaceHeaders(version: string, productService: IProductService, environmentService: IEnvironmentService, configurationService: IConfigurationService, fileService: IFileService, storageService: IStorageService | undefined): Promise<IHeaders> {
|
||||
const headers: IHeaders = {
|
||||
'X-Market-Client-Id': `VSCode ${version}`,
|
||||
'User-Agent': `VSCode ${version} (${productService.nameShort})`
|
||||
};
|
||||
const uuid = await getServiceMachineId(environmentService, fileService, storageService);
|
||||
if (supportsTelemetry(productService, environmentService) && getTelemetryLevel(configurationService) === TelemetryLevel.USAGE) {
|
||||
headers['X-Market-User-Id'] = uuid;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
@@ -7,12 +7,9 @@ import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { generateUuid, isUUID } from 'vs/base/common/uuid';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
|
||||
export async function getServiceMachineId(environmentService: IEnvironmentService, fileService: IFileService, storageService: {
|
||||
get: (key: string, scope: StorageScope, fallbackValue?: string | undefined) => string | undefined,
|
||||
store: (key: string, value: string, scope: StorageScope, target: StorageTarget) => void
|
||||
} | undefined): Promise<string> {
|
||||
export async function getServiceMachineId(environmentService: IEnvironmentService, fileService: IFileService, storageService: IStorageService | undefined): Promise<string> {
|
||||
let uuid: string | null = storageService ? storageService.get('storage.serviceMachineId', StorageScope.GLOBAL) || null : null;
|
||||
if (uuid) {
|
||||
return uuid;
|
||||
@@ -15,9 +15,9 @@ export interface IExternalTerminalSettings {
|
||||
}
|
||||
|
||||
export interface ITerminalForPlatform {
|
||||
windows: string,
|
||||
linux: string,
|
||||
osx: string
|
||||
windows: string;
|
||||
linux: string;
|
||||
osx: string;
|
||||
}
|
||||
|
||||
export interface IExternalTerminalService {
|
||||
@@ -29,7 +29,7 @@ export interface IExternalTerminalService {
|
||||
|
||||
export interface IExternalTerminalConfiguration {
|
||||
terminal: {
|
||||
explorerKind: 'integrated' | 'external',
|
||||
explorerKind: 'integrated' | 'external';
|
||||
external: IExternalTerminalSettings;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,63 +3,51 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { deepEqual, equal } from 'assert';
|
||||
import { DEFAULT_TERMINAL_OSX } from 'vs/platform/externalTerminal/common/externalTerminal';
|
||||
import { deepStrictEqual, strictEqual } from 'assert';
|
||||
import { DEFAULT_TERMINAL_OSX, IExternalTerminalConfiguration } from 'vs/platform/externalTerminal/common/externalTerminal';
|
||||
import { LinuxExternalTerminalService, MacExternalTerminalService, WindowsExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService';
|
||||
|
||||
const mockConfig = Object.freeze<IExternalTerminalConfiguration>({
|
||||
terminal: {
|
||||
explorerKind: 'external',
|
||||
external: {
|
||||
windowsExec: 'testWindowsShell',
|
||||
osxExec: 'testOSXShell',
|
||||
linuxExec: 'testLinuxShell'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
suite('ExternalTerminalService', () => {
|
||||
let mockOnExit: Function;
|
||||
let mockOnError: Function;
|
||||
let mockConfig: any;
|
||||
|
||||
setup(() => {
|
||||
mockConfig = {
|
||||
terminal: {
|
||||
explorerKind: 'external',
|
||||
external: {
|
||||
windowsExec: 'testWindowsShell',
|
||||
osxExec: 'testOSXShell',
|
||||
linuxExec: 'testLinuxShell'
|
||||
}
|
||||
}
|
||||
};
|
||||
mockOnExit = (s: any) => s;
|
||||
mockOnError = (e: any) => e;
|
||||
});
|
||||
|
||||
test(`WinTerminalService - uses terminal from configuration`, done => {
|
||||
let testShell = 'cmd';
|
||||
let testCwd = 'path/to/workspace';
|
||||
let mockSpawner = {
|
||||
const testShell = 'cmd';
|
||||
const testCwd = 'path/to/workspace';
|
||||
const mockSpawner: any = {
|
||||
spawn: (command: any, args: any, opts: any) => {
|
||||
// assert
|
||||
equal(command, testShell, 'shell should equal expected');
|
||||
equal(args[args.length - 1], mockConfig.terminal.external.windowsExec, 'terminal should equal expected');
|
||||
equal(opts.cwd, testCwd, 'opts.cwd should equal expected');
|
||||
strictEqual(command, testShell, 'shell should equal expected');
|
||||
strictEqual(args[args.length - 1], mockConfig.terminal.external.windowsExec);
|
||||
strictEqual(opts.cwd, testCwd);
|
||||
done();
|
||||
return {
|
||||
on: (evt: any) => evt
|
||||
};
|
||||
}
|
||||
};
|
||||
let testService = new WindowsExternalTerminalService();
|
||||
(<any>testService).spawnTerminal(
|
||||
const testService = new WindowsExternalTerminalService();
|
||||
testService.spawnTerminal(
|
||||
mockSpawner,
|
||||
mockConfig,
|
||||
mockConfig.terminal.external,
|
||||
testShell,
|
||||
testCwd,
|
||||
mockOnExit,
|
||||
mockOnError
|
||||
testCwd
|
||||
);
|
||||
});
|
||||
|
||||
test(`WinTerminalService - uses default terminal when configuration.terminal.external.windowsExec is undefined`, done => {
|
||||
let testShell = 'cmd';
|
||||
let testCwd = 'path/to/workspace';
|
||||
let mockSpawner = {
|
||||
const testShell = 'cmd';
|
||||
const testCwd = 'path/to/workspace';
|
||||
const mockSpawner: any = {
|
||||
spawn: (command: any, args: any, opts: any) => {
|
||||
// assert
|
||||
equal(args[args.length - 1], WindowsExternalTerminalService.getDefaultTerminalWindows(), 'terminal should equal expected');
|
||||
strictEqual(args[args.length - 1], WindowsExternalTerminalService.getDefaultTerminalWindows());
|
||||
done();
|
||||
return {
|
||||
on: (evt: any) => evt
|
||||
@@ -67,162 +55,139 @@ suite('ExternalTerminalService', () => {
|
||||
}
|
||||
};
|
||||
mockConfig.terminal.external.windowsExec = undefined;
|
||||
let testService = new WindowsExternalTerminalService();
|
||||
(<any>testService).spawnTerminal(
|
||||
const testService = new WindowsExternalTerminalService();
|
||||
testService.spawnTerminal(
|
||||
mockSpawner,
|
||||
mockConfig,
|
||||
mockConfig.terminal.external,
|
||||
testShell,
|
||||
testCwd,
|
||||
mockOnExit,
|
||||
mockOnError
|
||||
testCwd
|
||||
);
|
||||
});
|
||||
|
||||
test(`WinTerminalService - uses default terminal when configuration.terminal.external.windowsExec is undefined`, done => {
|
||||
let testShell = 'cmd';
|
||||
let testCwd = 'c:/foo';
|
||||
let mockSpawner = {
|
||||
const testShell = 'cmd';
|
||||
const testCwd = 'c:/foo';
|
||||
const mockSpawner: any = {
|
||||
spawn: (command: any, args: any, opts: any) => {
|
||||
// assert
|
||||
equal(opts.cwd, 'C:/foo', 'cwd should be uppercase regardless of the case that\'s passed in');
|
||||
strictEqual(opts.cwd, 'C:/foo', 'cwd should be uppercase regardless of the case that\'s passed in');
|
||||
done();
|
||||
return {
|
||||
on: (evt: any) => evt
|
||||
};
|
||||
}
|
||||
};
|
||||
let testService = new WindowsExternalTerminalService();
|
||||
(<any>testService).spawnTerminal(
|
||||
const testService = new WindowsExternalTerminalService();
|
||||
testService.spawnTerminal(
|
||||
mockSpawner,
|
||||
mockConfig,
|
||||
mockConfig.terminal.external,
|
||||
testShell,
|
||||
testCwd,
|
||||
mockOnExit,
|
||||
mockOnError
|
||||
testCwd
|
||||
);
|
||||
});
|
||||
|
||||
test(`WinTerminalService - cmder should be spawned differently`, done => {
|
||||
let testShell = 'cmd';
|
||||
mockConfig.terminal.external.windowsExec = 'cmder';
|
||||
let testCwd = 'c:/foo';
|
||||
let mockSpawner = {
|
||||
const testShell = 'cmd';
|
||||
const testCwd = 'c:/foo';
|
||||
const mockSpawner: any = {
|
||||
spawn: (command: any, args: any, opts: any) => {
|
||||
// assert
|
||||
deepEqual(args, ['C:/foo']);
|
||||
equal(opts, undefined);
|
||||
deepStrictEqual(args, ['C:/foo']);
|
||||
strictEqual(opts, undefined);
|
||||
done();
|
||||
return { on: (evt: any) => evt };
|
||||
}
|
||||
};
|
||||
let testService = new WindowsExternalTerminalService();
|
||||
(<any>testService).spawnTerminal(
|
||||
const testService = new WindowsExternalTerminalService();
|
||||
testService.spawnTerminal(
|
||||
mockSpawner,
|
||||
mockConfig,
|
||||
{ windowsExec: 'cmder' },
|
||||
testShell,
|
||||
testCwd,
|
||||
mockOnExit,
|
||||
mockOnError
|
||||
testCwd
|
||||
);
|
||||
});
|
||||
|
||||
test(`WinTerminalService - windows terminal should open workspace directory`, done => {
|
||||
let testShell = 'wt';
|
||||
let testCwd = 'c:/foo';
|
||||
let mockSpawner = {
|
||||
const testShell = 'wt';
|
||||
const testCwd = 'c:/foo';
|
||||
const mockSpawner: any = {
|
||||
spawn: (command: any, args: any, opts: any) => {
|
||||
// assert
|
||||
equal(opts.cwd, 'C:/foo');
|
||||
strictEqual(opts.cwd, 'C:/foo');
|
||||
done();
|
||||
return { on: (evt: any) => evt };
|
||||
}
|
||||
};
|
||||
let testService = new WindowsExternalTerminalService();
|
||||
(<any>testService).spawnTerminal(
|
||||
const testService = new WindowsExternalTerminalService();
|
||||
testService.spawnTerminal(
|
||||
mockSpawner,
|
||||
mockConfig,
|
||||
mockConfig.terminal.external,
|
||||
testShell,
|
||||
testCwd,
|
||||
mockOnExit,
|
||||
mockOnError
|
||||
testCwd
|
||||
);
|
||||
});
|
||||
|
||||
test(`MacTerminalService - uses terminal from configuration`, done => {
|
||||
let testCwd = 'path/to/workspace';
|
||||
let mockSpawner = {
|
||||
const testCwd = 'path/to/workspace';
|
||||
const mockSpawner: any = {
|
||||
spawn: (command: any, args: any, opts: any) => {
|
||||
// assert
|
||||
equal(args[1], mockConfig.terminal.external.osxExec, 'terminal should equal expected');
|
||||
strictEqual(args[1], mockConfig.terminal.external.osxExec);
|
||||
done();
|
||||
return {
|
||||
on: (evt: any) => evt
|
||||
};
|
||||
}
|
||||
};
|
||||
let testService = new MacExternalTerminalService();
|
||||
(<any>testService).spawnTerminal(
|
||||
const testService = new MacExternalTerminalService();
|
||||
testService.spawnTerminal(
|
||||
mockSpawner,
|
||||
mockConfig,
|
||||
testCwd,
|
||||
mockOnExit,
|
||||
mockOnError
|
||||
mockConfig.terminal.external,
|
||||
testCwd
|
||||
);
|
||||
});
|
||||
|
||||
test(`MacTerminalService - uses default terminal when configuration.terminal.external.osxExec is undefined`, done => {
|
||||
let testCwd = 'path/to/workspace';
|
||||
let mockSpawner = {
|
||||
const testCwd = 'path/to/workspace';
|
||||
const mockSpawner: any = {
|
||||
spawn: (command: any, args: any, opts: any) => {
|
||||
// assert
|
||||
equal(args[1], DEFAULT_TERMINAL_OSX, 'terminal should equal expected');
|
||||
strictEqual(args[1], DEFAULT_TERMINAL_OSX);
|
||||
done();
|
||||
return {
|
||||
on: (evt: any) => evt
|
||||
};
|
||||
}
|
||||
};
|
||||
mockConfig.terminal.external.osxExec = undefined;
|
||||
let testService = new MacExternalTerminalService();
|
||||
(<any>testService).spawnTerminal(
|
||||
const testService = new MacExternalTerminalService();
|
||||
testService.spawnTerminal(
|
||||
mockSpawner,
|
||||
mockConfig,
|
||||
testCwd,
|
||||
mockOnExit,
|
||||
mockOnError
|
||||
{ osxExec: undefined },
|
||||
testCwd
|
||||
);
|
||||
});
|
||||
|
||||
test(`LinuxTerminalService - uses terminal from configuration`, done => {
|
||||
let testCwd = 'path/to/workspace';
|
||||
let mockSpawner = {
|
||||
const testCwd = 'path/to/workspace';
|
||||
const mockSpawner: any = {
|
||||
spawn: (command: any, args: any, opts: any) => {
|
||||
// assert
|
||||
equal(command, mockConfig.terminal.external.linuxExec, 'terminal should equal expected');
|
||||
equal(opts.cwd, testCwd, 'opts.cwd should equal expected');
|
||||
strictEqual(command, mockConfig.terminal.external.linuxExec);
|
||||
strictEqual(opts.cwd, testCwd);
|
||||
done();
|
||||
return {
|
||||
on: (evt: any) => evt
|
||||
};
|
||||
}
|
||||
};
|
||||
let testService = new LinuxExternalTerminalService();
|
||||
(<any>testService).spawnTerminal(
|
||||
const testService = new LinuxExternalTerminalService();
|
||||
testService.spawnTerminal(
|
||||
mockSpawner,
|
||||
mockConfig,
|
||||
testCwd,
|
||||
mockOnExit,
|
||||
mockOnError
|
||||
mockConfig.terminal.external,
|
||||
testCwd
|
||||
);
|
||||
});
|
||||
|
||||
test(`LinuxTerminalService - uses default terminal when configuration.terminal.external.linuxExec is undefined`, done => {
|
||||
LinuxExternalTerminalService.getDefaultTerminalLinuxReady().then(defaultTerminalLinux => {
|
||||
let testCwd = 'path/to/workspace';
|
||||
let mockSpawner = {
|
||||
const testCwd = 'path/to/workspace';
|
||||
const mockSpawner: any = {
|
||||
spawn: (command: any, args: any, opts: any) => {
|
||||
// assert
|
||||
equal(command, defaultTerminalLinux, 'terminal should equal expected');
|
||||
strictEqual(command, defaultTerminalLinux);
|
||||
done();
|
||||
return {
|
||||
on: (evt: any) => evt
|
||||
@@ -230,13 +195,11 @@ suite('ExternalTerminalService', () => {
|
||||
}
|
||||
};
|
||||
mockConfig.terminal.external.linuxExec = undefined;
|
||||
let testService = new LinuxExternalTerminalService();
|
||||
(<any>testService).spawnTerminal(
|
||||
const testService = new LinuxExternalTerminalService();
|
||||
testService.spawnTerminal(
|
||||
mockSpawner,
|
||||
mockConfig,
|
||||
testCwd,
|
||||
mockOnExit,
|
||||
mockOnError
|
||||
mockConfig.terminal.external,
|
||||
testCwd
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -320,7 +320,7 @@ function getSanitizedEnvironment(process: NodeJS.Process) {
|
||||
/**
|
||||
* tries to turn OS errors into more meaningful error messages
|
||||
*/
|
||||
function improveError(err: Error & { errno?: string, path?: string }): Error {
|
||||
function improveError(err: Error & { errno?: string; path?: string }): Error {
|
||||
if ('errno' in err && err['errno'] === 'ENOENT' && 'path' in err && typeof err['path'] === 'string') {
|
||||
return new Error(nls.localize('ext.term.app.not.found', "can't find terminal application '{0}'", err['path']));
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user