Merge from vscode 2c306f762bf9c3db82dc06c7afaa56ef46d72f79 (#14050)

* Merge from vscode 2c306f762bf9c3db82dc06c7afaa56ef46d72f79

* Fix breaks

* Extension management fixes

* Fix breaks in windows bundling

* Fix/skip failing tests

* Update distro

* Add clear to nuget.config

* Add hygiene task

* Bump distro

* Fix hygiene issue

* Add build to hygiene exclusion

* Update distro

* Update hygiene

* Hygiene exclusions

* Update tsconfig

* Bump distro for server breaks

* Update build config

* Update darwin path

* Add done calls to notebook tests

* Skip failing tests

* Disable smoke tests
This commit is contained in:
Karl Burtram
2021-02-09 16:15:05 -08:00
committed by GitHub
parent 6f192f9af5
commit ce612a3d96
1929 changed files with 68012 additions and 34564 deletions

View File

@@ -0,0 +1,474 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/runtimeExtensionsEditor';
import * as nls from 'vs/nls';
import { Action, IAction, Separator } from 'vs/base/common/actions';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IExtensionService, IExtensionsStatus, IExtensionHostProfile } from 'vs/workbench/services/extensions/common/extensions';
import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list';
import { WorkbenchList } from 'vs/platform/list/browser/listService';
import { append, $, reset, Dimension, clearNode } from 'vs/base/browser/dom';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { RunOnceScheduler } from 'vs/base/common/async';
import { EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { memoize } from 'vs/base/common/decorators';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { Event } from 'vs/base/common/event';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ILabelService } from 'vs/platform/label/common/label';
import { renderCodicons } from 'vs/base/browser/codicons';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { Schemas } from 'vs/base/common/network';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { editorBackground } from 'vs/platform/theme/common/colorRegistry';
import { domEvent } from 'vs/base/browser/event';
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput';
interface IExtensionProfileInformation {
/**
* segment when the extension was running.
* 2*i = segment start time
* 2*i+1 = segment end time
*/
segments: number[];
/**
* total time when the extension was running.
* (sum of all segment lengths).
*/
totalTime: number;
}
export interface IRuntimeExtension {
originalIndex: number;
description: IExtensionDescription;
marketplaceInfo: IExtension;
status: IExtensionsStatus;
profileInfo?: IExtensionProfileInformation;
unresponsiveProfile?: IExtensionHostProfile;
}
export abstract class AbstractRuntimeExtensionsEditor extends EditorPane {
public static readonly ID: string = 'workbench.editor.runtimeExtensions';
private _list: WorkbenchList<IRuntimeExtension> | null;
private _elements: IRuntimeExtension[] | null;
private _updateSoon: RunOnceScheduler;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IContextKeyService contextKeyService: IContextKeyService,
@IExtensionsWorkbenchService private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionService private readonly _extensionService: IExtensionService,
@INotificationService private readonly _notificationService: INotificationService,
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
@IInstantiationService protected readonly _instantiationService: IInstantiationService,
@IStorageService storageService: IStorageService,
@ILabelService private readonly _labelService: ILabelService,
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
) {
super(AbstractRuntimeExtensionsEditor.ID, telemetryService, themeService, storageService);
this._list = null;
this._elements = null;
this._updateSoon = this._register(new RunOnceScheduler(() => this._updateExtensions(), 200));
this._register(this._extensionService.onDidChangeExtensionsStatus(() => this._updateSoon.schedule()));
this._updateExtensions();
}
protected async _updateExtensions(): Promise<void> {
this._elements = await this._resolveExtensions();
if (this._list) {
this._list.splice(0, this._list.length, this._elements);
}
}
private async _resolveExtensions(): Promise<IRuntimeExtension[]> {
// We only deal with extensions with source code!
const extensionsDescriptions = (await this._extensionService.getExtensions()).filter((extension) => {
return Boolean(extension.main) || Boolean(extension.browser);
});
let marketplaceMap: { [id: string]: IExtension; } = Object.create(null);
const marketPlaceExtensions = await this._extensionsWorkbenchService.queryLocal();
for (let extension of marketPlaceExtensions) {
marketplaceMap[ExtensionIdentifier.toKey(extension.identifier.id)] = extension;
}
let statusMap = this._extensionService.getExtensionsStatus();
// group profile segments by extension
let segments: { [id: string]: number[]; } = Object.create(null);
const profileInfo = this._getProfileInfo();
if (profileInfo) {
let currentStartTime = profileInfo.startTime;
for (let i = 0, len = profileInfo.deltas.length; i < len; i++) {
const id = profileInfo.ids[i];
const delta = profileInfo.deltas[i];
let extensionSegments = segments[ExtensionIdentifier.toKey(id)];
if (!extensionSegments) {
extensionSegments = [];
segments[ExtensionIdentifier.toKey(id)] = extensionSegments;
}
extensionSegments.push(currentStartTime);
currentStartTime = currentStartTime + delta;
extensionSegments.push(currentStartTime);
}
}
let result: IRuntimeExtension[] = [];
for (let i = 0, len = extensionsDescriptions.length; i < len; i++) {
const extensionDescription = extensionsDescriptions[i];
let profileInfo: IExtensionProfileInformation | null = null;
if (profileInfo) {
let extensionSegments = segments[ExtensionIdentifier.toKey(extensionDescription.identifier)] || [];
let extensionTotalTime = 0;
for (let j = 0, lenJ = extensionSegments.length / 2; j < lenJ; j++) {
const startTime = extensionSegments[2 * j];
const endTime = extensionSegments[2 * j + 1];
extensionTotalTime += (endTime - startTime);
}
profileInfo = {
segments: extensionSegments,
totalTime: extensionTotalTime
};
}
result[i] = {
originalIndex: i,
description: extensionDescription,
marketplaceInfo: marketplaceMap[ExtensionIdentifier.toKey(extensionDescription.identifier)],
status: statusMap[extensionDescription.identifier.value],
profileInfo: profileInfo || undefined,
unresponsiveProfile: this._getUnresponsiveProfile(extensionDescription.identifier)
};
}
result = result.filter(element => element.status.activationTimes);
// bubble up extensions that have caused slowness
const isUnresponsive = (extension: IRuntimeExtension): boolean =>
extension.unresponsiveProfile === profileInfo;
const profileTime = (extension: IRuntimeExtension): number =>
extension.profileInfo?.totalTime ?? 0;
const activationTime = (extension: IRuntimeExtension): number =>
(extension.status.activationTimes?.codeLoadingTime ?? 0) +
(extension.status.activationTimes?.activateCallTime ?? 0);
result = result.sort((a, b) => {
if (isUnresponsive(a) || isUnresponsive(b)) {
return +isUnresponsive(b) - +isUnresponsive(a);
} else if (profileTime(a) || profileTime(b)) {
return profileTime(b) - profileTime(a);
} else if (activationTime(a) || activationTime(b)) {
return activationTime(b) - activationTime(a);
}
return a.originalIndex - b.originalIndex;
});
return result;
}
protected createEditor(parent: HTMLElement): void {
parent.classList.add('runtime-extensions-editor');
const TEMPLATE_ID = 'runtimeExtensionElementTemplate';
const delegate = new class implements IListVirtualDelegate<IRuntimeExtension>{
getHeight(element: IRuntimeExtension): number {
return 62;
}
getTemplateId(element: IRuntimeExtension): string {
return TEMPLATE_ID;
}
};
interface IRuntimeExtensionTemplateData {
root: HTMLElement;
element: HTMLElement;
icon: HTMLImageElement;
name: HTMLElement;
version: HTMLElement;
msgContainer: HTMLElement;
actionbar: ActionBar;
activationTime: HTMLElement;
profileTime: HTMLElement;
disposables: IDisposable[];
elementDisposables: IDisposable[];
}
const renderer: IListRenderer<IRuntimeExtension, IRuntimeExtensionTemplateData> = {
templateId: TEMPLATE_ID,
renderTemplate: (root: HTMLElement): IRuntimeExtensionTemplateData => {
const element = append(root, $('.extension'));
const iconContainer = append(element, $('.icon-container'));
const icon = append(iconContainer, $<HTMLImageElement>('img.icon'));
const desc = append(element, $('div.desc'));
const headerContainer = append(desc, $('.header-container'));
const header = append(headerContainer, $('.header'));
const name = append(header, $('div.name'));
const version = append(header, $('span.version'));
const msgContainer = append(desc, $('div.msg'));
const actionbar = new ActionBar(desc, { animated: false });
actionbar.onDidRun(({ error }) => error && this._notificationService.error(error));
const timeContainer = append(element, $('.time'));
const activationTime = append(timeContainer, $('div.activation-time'));
const profileTime = append(timeContainer, $('div.profile-time'));
const disposables = [actionbar];
return {
root,
element,
icon,
name,
version,
actionbar,
activationTime,
profileTime,
msgContainer,
disposables,
elementDisposables: [],
};
},
renderElement: (element: IRuntimeExtension, index: number, data: IRuntimeExtensionTemplateData): void => {
data.elementDisposables = dispose(data.elementDisposables);
data.root.classList.toggle('odd', index % 2 === 1);
const onError = Event.once(domEvent(data.icon, 'error'));
onError(() => data.icon.src = element.marketplaceInfo.iconUrlFallback, null, data.elementDisposables);
data.icon.src = element.marketplaceInfo.iconUrl;
if (!data.icon.complete) {
data.icon.style.visibility = 'hidden';
data.icon.onload = () => data.icon.style.visibility = 'inherit';
} else {
data.icon.style.visibility = 'inherit';
}
data.name.textContent = element.marketplaceInfo.displayName;
data.version.textContent = element.description.version;
const activationTimes = element.status.activationTimes!;
let syncTime = activationTimes.codeLoadingTime + activationTimes.activateCallTime;
data.activationTime.textContent = activationTimes.activationReason.startup ? `Startup Activation: ${syncTime}ms` : `Activation: ${syncTime}ms`;
data.actionbar.clear();
const slowExtensionAction = this._createSlowExtensionAction(element);
if (slowExtensionAction) {
data.actionbar.push(slowExtensionAction, { icon: true, label: true });
}
if (isNonEmptyArray(element.status.runtimeErrors)) {
const reportExtensionIssueAction = this._createReportExtensionIssueAction(element);
if (reportExtensionIssueAction) {
data.actionbar.push(reportExtensionIssueAction, { icon: true, label: true });
}
}
let title: string;
const activationId = activationTimes.activationReason.extensionId.value;
const activationEvent = activationTimes.activationReason.activationEvent;
if (activationEvent === '*') {
title = nls.localize('starActivation', "Activated by {0} on start-up", activationId);
} else if (/^workspaceContains:/.test(activationEvent)) {
let fileNameOrGlob = activationEvent.substr('workspaceContains:'.length);
if (fileNameOrGlob.indexOf('*') >= 0 || fileNameOrGlob.indexOf('?') >= 0) {
title = nls.localize({
key: 'workspaceContainsGlobActivation',
comment: [
'{0} will be a glob pattern'
]
}, "Activated by {1} because a file matching {1} exists in your workspace", fileNameOrGlob, activationId);
} else {
title = nls.localize({
key: 'workspaceContainsFileActivation',
comment: [
'{0} will be a file name'
]
}, "Activated by {1} because file {0} exists in your workspace", fileNameOrGlob, activationId);
}
} else if (/^workspaceContainsTimeout:/.test(activationEvent)) {
const glob = activationEvent.substr('workspaceContainsTimeout:'.length);
title = nls.localize({
key: 'workspaceContainsTimeout',
comment: [
'{0} will be a glob pattern'
]
}, "Activated by {1} because searching for {0} took too long", glob, activationId);
} else if (activationEvent === 'onStartupFinished') {
title = nls.localize({
key: 'startupFinishedActivation',
comment: [
'This refers to an extension. {0} will be an activation event.'
]
}, "Activated by {0} after start-up finished", activationId);
} else if (/^onLanguage:/.test(activationEvent)) {
let language = activationEvent.substr('onLanguage:'.length);
title = nls.localize('languageActivation', "Activated by {1} because you opened a {0} file", language, activationId);
} else {
title = nls.localize({
key: 'workspaceGenericActivation',
comment: [
'The {0} placeholder will be an activation event, like e.g. \'language:typescript\', \'debug\', etc.'
]
}, "Activated by {1} on {0}", activationEvent, activationId);
}
data.activationTime.title = title;
clearNode(data.msgContainer);
if (this._getUnresponsiveProfile(element.description.identifier)) {
const el = $('span', undefined, ...renderCodicons(` $(alert) Unresponsive`));
el.title = nls.localize('unresponsive.title', "Extension has caused the extension host to freeze.");
data.msgContainer.appendChild(el);
}
if (isNonEmptyArray(element.status.runtimeErrors)) {
const el = $('span', undefined, ...renderCodicons(`$(bug) ${nls.localize('errors', "{0} uncaught errors", element.status.runtimeErrors.length)}`));
data.msgContainer.appendChild(el);
}
if (element.status.messages && element.status.messages.length > 0) {
const el = $('span', undefined, ...renderCodicons(`$(alert) ${element.status.messages[0].message}`));
data.msgContainer.appendChild(el);
}
if (element.description.extensionLocation.scheme !== Schemas.file) {
const el = $('span', undefined, ...renderCodicons(`$(remote) ${element.description.extensionLocation.authority}`));
data.msgContainer.appendChild(el);
const hostLabel = this._labelService.getHostLabel(Schemas.vscodeRemote, this._environmentService.remoteAuthority);
if (hostLabel) {
reset(el, ...renderCodicons(`$(remote) ${hostLabel}`));
}
}
if (element.profileInfo) {
data.profileTime.textContent = `Profile: ${(element.profileInfo.totalTime / 1000).toFixed(2)}ms`;
} else {
data.profileTime.textContent = '';
}
},
disposeTemplate: (data: IRuntimeExtensionTemplateData): void => {
data.disposables = dispose(data.disposables);
}
};
this._list = <WorkbenchList<IRuntimeExtension>>this._instantiationService.createInstance(WorkbenchList,
'RuntimeExtensions',
parent, delegate, [renderer], {
multipleSelectionSupport: false,
setRowLineHeight: false,
horizontalScrolling: false,
overrideStyles: {
listBackground: editorBackground
},
accessibilityProvider: new class implements IListAccessibilityProvider<IRuntimeExtension> {
getWidgetAriaLabel(): string {
return nls.localize('runtimeExtensions', "Runtime Extensions");
}
getAriaLabel(element: IRuntimeExtension): string | null {
return element.description.name;
}
}
});
this._list.splice(0, this._list.length, this._elements || undefined);
this._list.onContextMenu((e) => {
if (!e.element) {
return;
}
const actions: IAction[] = [];
const reportExtensionIssueAction = this._createReportExtensionIssueAction(e.element);
if (reportExtensionIssueAction) {
actions.push(reportExtensionIssueAction);
actions.push(new Separator());
}
actions.push(new Action('runtimeExtensionsEditor.action.disableWorkspace', nls.localize('disable workspace', "Disable (Workspace)"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo, EnablementState.DisabledWorkspace)));
actions.push(new Action('runtimeExtensionsEditor.action.disable', nls.localize('disable', "Disable"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo, EnablementState.DisabledGlobally)));
actions.push(new Separator());
const profileAction = this._createProfileAction();
if (profileAction) {
actions.push(profileAction);
}
const saveExtensionHostProfileAction = this.saveExtensionHostProfileAction;
if (saveExtensionHostProfileAction) {
actions.push(saveExtensionHostProfileAction);
}
this._contextMenuService.showContextMenu({
getAnchor: () => e.anchor,
getActions: () => actions
});
});
}
@memoize
private get saveExtensionHostProfileAction(): IAction | null {
return this._createSaveExtensionHostProfileAction();
}
public layout(dimension: Dimension): void {
if (this._list) {
this._list.layout(dimension.height);
}
}
protected abstract _getProfileInfo(): IExtensionHostProfile | null;
protected abstract _getUnresponsiveProfile(extensionId: ExtensionIdentifier): IExtensionHostProfile | undefined;
protected abstract _createSlowExtensionAction(element: IRuntimeExtension): Action | null;
protected abstract _createReportExtensionIssueAction(element: IRuntimeExtension): Action | null;
protected abstract _createSaveExtensionHostProfileAction(): Action | null;
protected abstract _createProfileAction(): Action | null;
}
export class ShowRuntimeExtensionsAction extends Action {
static readonly ID = 'workbench.action.showRuntimeExtensions';
static readonly LABEL = nls.localize('showRuntimeExtensions', "Show Running Extensions");
constructor(
id: string, label: string,
@IEditorService private readonly _editorService: IEditorService
) {
super(id, label);
}
public async run(e?: any): Promise<any> {
await this._editorService.openEditor(RuntimeExtensionsInput.instance, { revealIfOpened: true, pinned: true });
}
}

View File

@@ -0,0 +1,62 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Action } from 'vs/base/common/actions';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IExtensionService, IExtensionHostProfile } from 'vs/workbench/services/extensions/common/extensions';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ILabelService } from 'vs/platform/label/common/label';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { AbstractRuntimeExtensionsEditor, IRuntimeExtension } from 'vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor';
export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor {
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IContextKeyService contextKeyService: IContextKeyService,
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionService extensionService: IExtensionService,
@INotificationService notificationService: INotificationService,
@IContextMenuService contextMenuService: IContextMenuService,
@IInstantiationService instantiationService: IInstantiationService,
@IStorageService storageService: IStorageService,
@ILabelService labelService: ILabelService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
) {
super(telemetryService, themeService, contextKeyService, extensionsWorkbenchService, extensionService, notificationService, contextMenuService, instantiationService, storageService, labelService, environmentService);
}
protected _getProfileInfo(): IExtensionHostProfile | null {
return null;
}
protected _getUnresponsiveProfile(extensionId: ExtensionIdentifier): IExtensionHostProfile | undefined {
return undefined;
}
protected _createSlowExtensionAction(element: IRuntimeExtension): Action | null {
return null;
}
protected _createReportExtensionIssueAction(element: IRuntimeExtension): Action | null {
return null;
}
protected _createSaveExtensionHostProfileAction(): Action | null {
return null;
}
protected _createProfileAction(): Action | null {
return null;
}
}

View File

@@ -4,9 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import { IExtensionTipsService, IConfigBasedExtensionTip } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { localize } from 'vs/nls';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { IWorkspaceContextService, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
import { Emitter } from 'vs/base/common/event';
@@ -27,11 +27,11 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations {
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return [...this.importantRecommendations, ...this.otherRecommendations]; }
constructor(
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService,
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
) {
super(promptedExtensionRecommendations);
// {{SQL CARBON EDIT}}
super(undefined);
}
protected async doActivate(): Promise<void> {
@@ -73,7 +73,6 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations {
private toExtensionRecommendation(tip: IConfigBasedExtensionTip): ExtensionRecommendation {
return {
extensionId: tip.extensionId,
source: 'config',
reason: {
reasonId: ExtensionRecommendationReason.WorkspaceConfig,
reasonText: localize('exeBasedRecommendation', "This extension is recommended because of the current workspace configuration")

View File

@@ -4,15 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import { IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { IFileService } from 'vs/platform/files/common/files';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { IWorkspaceTagsService } from 'vs/workbench/contrib/tags/common/workspaceTags';
import { isNumber } from 'vs/base/common/types';
import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { localize } from 'vs/nls';
type DynamicWorkspaceRecommendationsClassification = {
@@ -30,7 +30,6 @@ export class DynamicWorkspaceRecommendations extends ExtensionRecommendations {
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
constructor(
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService,
@IWorkspaceTagsService private readonly workspaceTagsService: IWorkspaceTagsService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@@ -38,7 +37,7 @@ export class DynamicWorkspaceRecommendations extends ExtensionRecommendations {
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IStorageService private readonly storageService: IStorageService,
) {
super(promptedExtensionRecommendations);
super();
}
protected async doActivate(): Promise<void> {
@@ -82,7 +81,7 @@ export class DynamicWorkspaceRecommendations extends ExtensionRecommendations {
const workspaceTip = workspacesTips.filter(workspaceTip => isNonEmptyArray(workspaceTip.remoteSet) && workspaceTip.remoteSet.indexOf(hashedRemote) > -1)[0];
if (workspaceTip) {
this._recommendations = workspaceTip.recommendations.map(id => this.toExtensionRecommendation(id, folder));
this.storageService.store(dynamicWorkspaceRecommendationsStorageKey, JSON.stringify(<IStoredDynamicWorkspaceRecommendations>{ recommendations: workspaceTip.recommendations, timestamp: Date.now() }), StorageScope.WORKSPACE);
this.storageService.store(dynamicWorkspaceRecommendationsStorageKey, JSON.stringify(<IStoredDynamicWorkspaceRecommendations>{ recommendations: workspaceTip.recommendations, timestamp: Date.now() }), StorageScope.WORKSPACE, StorageTarget.MACHINE);
this.telemetryService.publicLog2<{ count: number, cache: number }, DynamicWorkspaceRecommendationsClassification>('dynamicWorkspaceRecommendations', { count: this._recommendations.length, cache: 0 });
return;
}
@@ -107,7 +106,6 @@ export class DynamicWorkspaceRecommendations extends ExtensionRecommendations {
private toExtensionRecommendation(extensionId: string, folder: IWorkspaceFolder): ExtensionRecommendation {
return {
extensionId: extensionId.toLowerCase(),
source: 'dynamic',
reason: {
reasonId: ExtensionRecommendationReason.DynamicWorkspace,
reasonText: localize('dynamicWorkspaceRecommendation', "This extension may interest you because it's popular among users of the {0} repository.", folder.name)

View File

@@ -3,20 +3,11 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IExtensionTipsService, IExecutableBasedExtensionTip, IExtensionManagementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { timeout } from 'vs/base/common/async';
import { IExtensionTipsService, IExecutableBasedExtensionTip } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { localize } from 'vs/nls';
import { optional } from 'vs/platform/instantiation/common/instantiation';
import { basename } from 'vs/base/common/path';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService';
type ExeExtensionRecommendationsClassification = {
extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
exeName: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
};
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
export class ExeBasedRecommendations extends ExtensionRecommendations {
@@ -28,23 +19,10 @@ export class ExeBasedRecommendations extends ExtensionRecommendations {
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return [...this.importantRecommendations, ...this.otherRecommendations]; }
private readonly tasExperimentService: ITASExperimentService | undefined;
constructor(
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService,
@optional(ITASExperimentService) tasExperimentService: ITASExperimentService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
) {
super(promptedExtensionRecommendations);
this.tasExperimentService = tasExperimentService;
/*
3s has come out to be the good number to fetch and prompt important exe based recommendations
Also fetch important exe based recommendations for reporting telemetry
*/
timeout(3000).then(() => this.fetchAndPromptImportantExeBasedRecommendations());
super();
}
getRecommendations(exe: string): { important: ExtensionRecommendation[], others: ExtensionRecommendation[] } {
@@ -79,79 +57,9 @@ export class ExeBasedRecommendations extends ExtensionRecommendations {
return importantExeBasedRecommendations;
}
private async fetchAndPromptImportantExeBasedRecommendations(): Promise<void> {
const importantExeBasedRecommendations = await this.fetchImportantExeBasedRecommendations();
const local = await this.extensionManagementService.getInstalled();
const { installed, uninstalled } = this.groupByInstalled([...importantExeBasedRecommendations.keys()], local);
/* Log installed and uninstalled exe based recommendations */
for (const extensionId of installed) {
const tip = importantExeBasedRecommendations.get(extensionId);
if (tip) {
this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:alreadyInstalled', { extensionId, exeName: basename(tip.windowsPath!) });
}
}
for (const extensionId of uninstalled) {
const tip = importantExeBasedRecommendations.get(extensionId);
if (tip) {
this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:notInstalled', { extensionId, exeName: basename(tip.windowsPath!) });
}
}
this.promptImportantExeBasedRecommendations(uninstalled, importantExeBasedRecommendations);
}
private async promptImportantExeBasedRecommendations(recommendations: string[], importantExeBasedRecommendations: Map<string, IExecutableBasedExtensionTip>): Promise<void> {
if (this.promptedExtensionRecommendations.hasToIgnoreRecommendationNotifications()) {
return;
}
recommendations = this.promptedExtensionRecommendations.filterIgnoredOrNotAllowed(recommendations);
if (recommendations.length === 0) {
return;
}
const recommendationsByExe = new Map<string, IExecutableBasedExtensionTip[]>();
for (const extensionId of recommendations) {
const tip = importantExeBasedRecommendations.get(extensionId);
if (tip) {
let tips = recommendationsByExe.get(tip.exeFriendlyName);
if (!tips) {
tips = [];
recommendationsByExe.set(tip.exeFriendlyName, tips);
}
tips.push(tip);
}
}
for (const [, tips] of recommendationsByExe) {
const extensionIds = tips.map(({ extensionId }) => extensionId.toLowerCase());
if (this.tasExperimentService && extensionIds.indexOf('ms-vscode-remote.remote-wsl') !== -1) {
await this.tasExperimentService.getTreatment<boolean>('wslpopupaa');
}
const message = localize('exeRecommended', "You have {0} installed on your system. Do you want to install the recommended extensions for it?", tips[0].exeFriendlyName);
this.promptedExtensionRecommendations.promptImportantExtensionsInstallNotification(extensionIds, message, `@exe:"${tips[0].exeName}"`);
}
}
private groupByInstalled(recommendationsToSuggest: string[], local: ILocalExtension[]): { installed: string[], uninstalled: string[] } {
const installed: string[] = [], uninstalled: string[] = [];
const installedExtensionsIds = local.reduce((result, i) => { result.add(i.identifier.id.toLowerCase()); return result; }, new Set<string>());
recommendationsToSuggest.forEach(id => {
if (installedExtensionsIds.has(id.toLowerCase())) {
installed.push(id);
} else {
uninstalled.push(id);
}
});
return { installed, uninstalled };
}
private toExtensionRecommendation(tip: IExecutableBasedExtensionTip): ExtensionRecommendation {
return {
extensionId: tip.extensionId.toLowerCase(),
source: 'executable',
reason: {
reasonId: ExtensionRecommendationReason.Executable,
reasonText: localize('exeBasedRecommendation', "This extension is recommended because you have {0} installed.", tip.exeFriendlyName || basename(tip.windowsPath!))

View File

@@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { IExperimentService, ExperimentActionType, ExperimentState } from 'vs/workbench/contrib/experiments/common/experimentService';
export class ExperimentalRecommendations extends ExtensionRecommendations {
@@ -14,10 +14,9 @@ export class ExperimentalRecommendations extends ExtensionRecommendations {
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
constructor(
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IExperimentService private readonly experimentService: IExperimentService,
) {
super(promptedExtensionRecommendations);
super();
}
/**
@@ -29,7 +28,6 @@ export class ExperimentalRecommendations extends ExtensionRecommendations {
if (state === ExperimentState.Run && isNonEmptyArray(action?.properties?.recommendations) && action?.properties?.recommendationReason) {
action.properties.recommendations.forEach((extensionId: string) => this._recommendations.push({
extensionId: extensionId.toLowerCase(),
source: 'experimental',
reason: {
reasonId: ExtensionRecommendationReason.Experimental,
reasonText: action.properties.recommendationReason

View File

@@ -14,20 +14,25 @@ import { Action, IAction } from 'vs/base/common/actions';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { dispose, toDisposable, Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { domEvent } from 'vs/base/browser/event';
import { append, $, finalHandler, join, hide, show, addDisposableListener, EventType } from 'vs/base/browser/dom';
import { append, $, finalHandler, join, hide, show, addDisposableListener, EventType, setParentFlowTo } from 'vs/base/browser/dom';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionManifest, IKeyBinding, IView, IViewContainer, ExtensionType } from 'vs/platform/extensions/common/extensions';
import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { IExtensionManifest, IKeyBinding, IView, IViewContainer } from 'vs/platform/extensions/common/extensions';
import { ResolvedKeybinding, KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, IExtension, ExtensionContainers } from 'vs/workbench/contrib/extensions/common/extensions';
import { /*RatingsWidget, InstallCountWidget, */RemoteBadgeWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets';
import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { CombinedInstallAction, UpdateAction, ExtensionEditorDropDownAction, ReloadAction, MaliciousStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, ExtensionToolTipAction, SystemDisabledWarningAction, LocalInstallAction, SyncIgnoredIconAction, SetProductIconThemeAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import {
UpdateAction, ReloadAction, MaliciousStatusLabelAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction,
RemoteInstallAction, ExtensionToolTipAction, SystemDisabledWarningAction, LocalInstallAction, ToggleSyncExtensionAction, SetProductIconThemeAction,
ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, UninstallAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction,
InstallAnotherVersionAction, ExtensionEditorManageExtensionAction
} from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener';
@@ -38,7 +43,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { Color } from 'vs/base/common/color';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { CancellationToken } from 'vs/base/common/cancellation';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { ExtensionsTree, ExtensionData, ExtensionsGridView, getExtensions } from 'vs/workbench/contrib/extensions/browser/extensionsViewer';
import { ShowCurrentReleaseNotesActionId } from 'vs/workbench/contrib/update/common/update';
import { KeybindingParser } from 'vs/base/common/keybindingParser';
@@ -60,6 +65,7 @@ import { TokenizationRegistry } from 'vs/editor/common/modes';
import { generateTokensCSSForColorMap } from 'vs/editor/common/modes/supports/tokenization';
import { editorBackground } from 'vs/platform/theme/common/colorRegistry';
import { registerAction2, Action2 } from 'vs/platform/actions/common/actions';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
function removeEmbeddedSVGs(documentContent: string): string {
const newDocument = new DOMParser().parseFromString(documentContent, 'text/html');
@@ -190,11 +196,13 @@ export class ExtensionEditor extends EditorPane {
@INotificationService private readonly notificationService: INotificationService,
@IOpenerService private readonly openerService: IOpenerService,
@IExtensionRecommendationsService private readonly extensionRecommendationsService: IExtensionRecommendationsService,
@IExtensionIgnoredRecommendationsService private readonly extensionIgnoredRecommendationsService: IExtensionIgnoredRecommendationsService,
@IStorageService storageService: IStorageService,
@IExtensionService private readonly extensionService: IExtensionService,
@IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService,
@IWebviewService private readonly webviewService: IWebviewService,
@IModeService private readonly modeService: IModeService,
@IContextMenuService private readonly contextMenuService: IContextMenuService,
) {
super(ExtensionEditor.ID, telemetryService, themeService, storageService);
this.extensionReadme = null;
@@ -250,9 +258,12 @@ export class ExtensionEditor extends EditorPane {
const extensionActionBar = this._register(new ActionBar(extensionActions, {
animated: false,
actionViewItemProvider: (action: IAction) => {
if (action instanceof ExtensionEditorDropDownAction) {
if (action instanceof ExtensionDropDownAction) {
return action.createActionViewItem();
}
if (action instanceof ActionWithDropDownAction) {
return new ExtensionActionWithDropdownActionViewItem(action, { icon: true, label: true, menuActionsOrProvider: { getActions: () => action.menuActions }, menuActionClassNames: (action.class || '').split(' ') }, this.contextMenuService);
}
return undefined;
}
}));
@@ -275,6 +286,7 @@ export class ExtensionEditor extends EditorPane {
const navbar = new NavBar(body);
const content = append(body, $('.content'));
content.id = generateUuid(); // An id is needed for the webview parent flow to
this.template = {
builtin,
@@ -322,8 +334,6 @@ export class ExtensionEditor extends EditorPane {
}
private async updateTemplate(input: ExtensionsInput, template: IExtensionEditorTemplate, preserveFocus: boolean): Promise<void> {
const runningExtensions = await this.extensionService.getExtensions();
this.activeElement = null;
this.editorLoadComplete = false;
const extension = input.extension;
@@ -342,7 +352,7 @@ export class ExtensionEditor extends EditorPane {
template.name.textContent = extension.displayName;
template.identifier.textContent = extension.identifier.id;
template.preview.style.display = extension.preview ? 'inherit' : 'none';
template.builtin.style.display = extension.type === ExtensionType.System ? 'inherit' : 'none';
template.builtin.style.display = extension.isBuiltin ? 'inherit' : 'none';
template.publisher.textContent = extension.publisherDisplayName;
template.version.textContent = `v${extension.version}`;
@@ -420,11 +430,10 @@ export class ExtensionEditor extends EditorPane {
// this.instantiationService.createInstance(RatingsWidget, template.rating, false) {{SQL CARBON EDIT}} Remove the widgets
];
const reloadAction = this.instantiationService.createInstance(ReloadAction);
const combinedInstallAction = this.instantiationService.createInstance(CombinedInstallAction);
const combinedInstallAction = this.instantiationService.createInstance(InstallDropdownAction);
const systemDisabledWarningAction = this.instantiationService.createInstance(SystemDisabledWarningAction);
const actions = [
reloadAction,
this.instantiationService.createInstance(SyncIgnoredIconAction),
this.instantiationService.createInstance(StatusLabelAction),
this.instantiationService.createInstance(UpdateAction),
this.instantiationService.createInstance(SetColorThemeAction, await this.workbenchThemeService.getColorThemes()),
@@ -432,11 +441,18 @@ export class ExtensionEditor extends EditorPane {
this.instantiationService.createInstance(SetProductIconThemeAction, await this.workbenchThemeService.getProductIconThemes()),
this.instantiationService.createInstance(EnableDropDownAction),
this.instantiationService.createInstance(DisableDropDownAction, runningExtensions),
this.instantiationService.createInstance(DisableDropDownAction),
this.instantiationService.createInstance(RemoteInstallAction, false),
this.instantiationService.createInstance(LocalInstallAction),
combinedInstallAction,
this.instantiationService.createInstance(InstallingLabelAction),
this.instantiationService.createInstance(ActionWithDropDownAction, 'extensions.uninstall', UninstallAction.UninstallLabel, [
this.instantiationService.createInstance(UninstallAction),
this.instantiationService.createInstance(InstallAnotherVersionAction),
]),
this.instantiationService.createInstance(ToggleSyncExtensionAction),
systemDisabledWarningAction,
this.instantiationService.createInstance(ExtensionEditorManageExtensionAction),
this.instantiationService.createInstance(ExtensionToolTipAction, systemDisabledWarningAction, reloadAction),
this.instantiationService.createInstance(MaliciousStatusLabelAction, true),
];
@@ -449,7 +465,7 @@ export class ExtensionEditor extends EditorPane {
this.transientDisposables.add(disposable);
}
this.setSubText(extension, reloadAction, template);
this.setSubText(extension, template);
template.content.innerText = ''; // Clear content before setting navbar actions.
template.navbar.clear();
@@ -480,65 +496,24 @@ export class ExtensionEditor extends EditorPane {
this.editorLoadComplete = true;
}
private setSubText(extension: IExtension, reloadAction: ReloadAction, template: IExtensionEditorTemplate): void {
private setSubText(extension: IExtension, template: IExtensionEditorTemplate): void {
hide(template.subtextContainer);
const ignoreAction = this.instantiationService.createInstance(IgnoreExtensionRecommendationAction, extension);
const undoIgnoreAction = this.instantiationService.createInstance(UndoIgnoreExtensionRecommendationAction, extension);
ignoreAction.enabled = false;
undoIgnoreAction.enabled = false;
template.ignoreActionbar.clear();
template.ignoreActionbar.push([ignoreAction, undoIgnoreAction], { icon: true, label: true });
this.transientDisposables.add(ignoreAction);
this.transientDisposables.add(undoIgnoreAction);
const extRecommendations = this.extensionRecommendationsService.getAllRecommendationsWithReason();
if (extRecommendations[extension.identifier.id.toLowerCase()]) {
ignoreAction.enabled = true;
template.subtext.textContent = extRecommendations[extension.identifier.id.toLowerCase()].reasonText;
show(template.subtextContainer);
} else if (this.extensionRecommendationsService.getIgnoredRecommendations().indexOf(extension.identifier.id.toLowerCase()) !== -1) {
undoIgnoreAction.enabled = true;
template.subtext.textContent = localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension.");
show(template.subtextContainer);
}
else {
template.subtext.textContent = '';
}
this.extensionRecommendationsService.onRecommendationChange(change => {
if (change.extensionId.toLowerCase() === extension.identifier.id.toLowerCase()) {
if (change.isRecommended) {
undoIgnoreAction.enabled = false;
const extRecommendations = this.extensionRecommendationsService.getAllRecommendationsWithReason();
if (extRecommendations[extension.identifier.id.toLowerCase()]) {
ignoreAction.enabled = true;
template.subtext.textContent = extRecommendations[extension.identifier.id.toLowerCase()].reasonText;
}
} else {
undoIgnoreAction.enabled = true;
ignoreAction.enabled = false;
template.subtext.textContent = localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension.");
}
}
});
this.transientDisposables.add(reloadAction.onDidChange(e => {
if (e.tooltip) {
template.subtext.textContent = reloadAction.tooltip;
const updateRecommendationFn = () => {
const extRecommendations = this.extensionRecommendationsService.getAllRecommendationsWithReason();
if (extRecommendations[extension.identifier.id.toLowerCase()]) {
template.subtext.textContent = extRecommendations[extension.identifier.id.toLowerCase()].reasonText;
show(template.subtextContainer);
ignoreAction.enabled = false;
undoIgnoreAction.enabled = false;
}
if (e.enabled === true) {
} else if (this.extensionIgnoredRecommendationsService.globalIgnoredRecommendations.indexOf(extension.identifier.id.toLowerCase()) !== -1) {
template.subtext.textContent = localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension.");
show(template.subtextContainer);
}
if (e.enabled === false) {
} else {
template.subtext.textContent = '';
hide(template.subtextContainer);
}
this.layout();
}));
};
updateRecommendationFn();
this.transientDisposables.add(this.extensionRecommendationsService.onDidChangeRecommendations(() => updateRecommendationFn()));
}
clearInput(): void {
@@ -584,8 +559,13 @@ export class ExtensionEditor extends EditorPane {
template.content.innerText = '';
this.activeElement = null;
if (id) {
this.open(id, extension, template)
const cts = new CancellationTokenSource();
this.contentDisposables.add(toDisposable(() => cts.dispose(true)));
this.open(id, extension, template, cts.token)
.then(activeElement => {
if (cts.token.isCancellationRequested) {
return;
}
this.activeElement = activeElement;
if (focus) {
this.focus();
@@ -594,26 +574,31 @@ export class ExtensionEditor extends EditorPane {
}
}
private open(id: string, extension: IExtension, template: IExtensionEditorTemplate): Promise<IActiveElement | null> {
private open(id: string, extension: IExtension, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {
switch (id) {
case NavbarSection.Readme: return this.openReadme(template);
case NavbarSection.Contributions: return this.openContributions(template);
case NavbarSection.Changelog: return this.openChangelog(template);
case NavbarSection.Dependencies: return this.openDependencies(extension, template);
case NavbarSection.Readme: return this.openReadme(template, token);
case NavbarSection.Contributions: return this.openContributions(template, token);
case NavbarSection.Changelog: return this.openChangelog(template, token);
case NavbarSection.Dependencies: return this.openDependencies(extension, template, token);
}
return Promise.resolve(null);
}
private async openMarkdown(cacheResult: CacheResult<string>, noContentCopy: string, template: IExtensionEditorTemplate): Promise<IActiveElement> {
private async openMarkdown(cacheResult: CacheResult<string>, noContentCopy: string, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {
try {
const body = await this.renderMarkdown(cacheResult, template);
if (token.isCancellationRequested) {
return Promise.resolve(null);
}
const webview = this.contentDisposables.add(this.webviewService.createWebviewOverlay('extensionEditor', {
enableFindWidget: true,
}, {}, undefined));
webview.claim(this);
webview.claim(this, this.scopedContextKeyService);
setParentFlowTo(webview.container, template.content);
webview.layoutWebviewOverElement(template.content);
webview.html = body;
this.contentDisposables.add(webview.onDidFocus(() => this.fireOnDidFocus()));
@@ -854,15 +839,19 @@ export class ExtensionEditor extends EditorPane {
</html>`;
}
private async openReadme(template: IExtensionEditorTemplate): Promise<IActiveElement> {
private async openReadme(template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {
const manifest = await this.extensionManifest!.get().promise;
if (manifest && manifest.extensionPack && manifest.extensionPack.length) {
return this.openExtensionPackReadme(manifest, template);
return this.openExtensionPackReadme(manifest, template, token);
}
return this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available."), template);
return this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available."), template, token);
}
private async openExtensionPackReadme(manifest: IExtensionManifest, template: IExtensionEditorTemplate): Promise<IActiveElement> {
private async openExtensionPackReadme(manifest: IExtensionManifest, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {
if (token.isCancellationRequested) {
return Promise.resolve(null);
}
const extensionPackReadme = append(template.content, $('div', { class: 'extension-pack-readme' }));
extensionPackReadme.style.margin = '0 auto';
extensionPackReadme.style.maxWidth = '882px';
@@ -886,21 +875,25 @@ export class ExtensionEditor extends EditorPane {
const readmeContent = append(extensionPackReadme, $('div.readme-content'));
await Promise.all([
this.renderExtensionPack(manifest, extensionPackContent),
this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available."), { ...template, ...{ content: readmeContent } }),
this.renderExtensionPack(manifest, extensionPackContent, token),
this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available."), { ...template, ...{ content: readmeContent } }, token),
]);
return { focus: () => extensionPackContent.focus() };
}
private openChangelog(template: IExtensionEditorTemplate): Promise<IActiveElement> {
return this.openMarkdown(this.extensionChangelog!.get(), localize('noChangelog', "No Changelog available."), template);
private openChangelog(template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {
return this.openMarkdown(this.extensionChangelog!.get(), localize('noChangelog', "No Changelog available."), template, token);
}
private openContributions(template: IExtensionEditorTemplate): Promise<IActiveElement> {
private openContributions(template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {
const content = $('div', { class: 'subcontent', tabindex: '0' });
return this.loadContents(() => this.extensionManifest!.get(), template)
.then(manifest => {
if (token.isCancellationRequested) {
return null;
}
if (!manifest) {
return content;
}
@@ -927,6 +920,7 @@ export class ExtensionEditor extends EditorPane {
renderDashboardContributions(content, manifest, layout), // {{SQL CARBON EDIT}}
this.renderCustomEditors(content, manifest, layout),
this.renderAuthentication(content, manifest, layout),
this.renderActivationEvents(content, manifest, layout),
];
scrollableContent.scanDomNode();
@@ -941,13 +935,21 @@ export class ExtensionEditor extends EditorPane {
}
return content;
}, () => {
if (token.isCancellationRequested) {
return null;
}
append(content, $('p.nocontent')).textContent = localize('noContributions', "No Contributions");
append(template.content, content);
return content;
});
}
private openDependencies(extension: IExtension, template: IExtensionEditorTemplate): Promise<IActiveElement> {
private openDependencies(extension: IExtension, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {
if (token.isCancellationRequested) {
return Promise.resolve(null);
}
if (arrays.isFalsyOrEmpty(extension.dependencies)) {
append(template.content, $('p.nocontent')).textContent = localize('noDependencies', "No Dependencies");
return Promise.resolve(template.content);
@@ -976,7 +978,11 @@ export class ExtensionEditor extends EditorPane {
return Promise.resolve({ focus() { dependenciesTree.domFocus(); } });
}
private async renderExtensionPack(manifest: IExtensionManifest, parent: HTMLElement): Promise<void> {
private async renderExtensionPack(manifest: IExtensionManifest, parent: HTMLElement, token: CancellationToken): Promise<void> {
if (token.isCancellationRequested) {
return;
}
const content = $('div', { class: 'subcontent' });
const scrollableContent = new DomScrollableElement(content, { useShadows: false });
append(parent, scrollableContent.getDomNode());
@@ -1441,6 +1447,21 @@ export class ExtensionEditor extends EditorPane {
return true;
}
private renderActivationEvents(container: HTMLElement, manifest: IExtensionManifest, onDetailsToggle: Function): boolean {
const activationEvents = manifest.activationEvents || [];
if (!activationEvents.length) {
return false;
}
const details = $('details', { open: true, ontoggle: onDetailsToggle },
$('summary', { tabindex: '0' }, localize('activation events', "Activation Events ({0})", activationEvents.length)),
$('ul', undefined, ...activationEvents.map(activationEvent => $('li', undefined, $('code', undefined, activationEvent))))
);
append(container, details);
return true;
}
private resolveKeybinding(rawKeyBinding: IKeyBinding): ResolvedKeybinding | null {
let key: string | undefined;

View File

@@ -0,0 +1,420 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IAction } from 'vs/base/common/actions';
import { distinct } from 'vs/base/common/arrays';
import { CancelablePromise, createCancelablePromise, raceCancellablePromises, raceCancellation, timeout } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
import { localize } from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation';
import { INotificationHandle, INotificationService, IPromptChoice, IPromptChoiceWithMenu, Severity } from 'vs/platform/notification/common/notification';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IUserDataAutoSyncEnablementService, IUserDataSyncResourceEnablementService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync';
import { SearchExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService';
import { EnablementState, IWorkbenchExtensioManagementService, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionIgnoredRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
type ExtensionRecommendationsNotificationClassification = {
userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
extensionId?: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
};
type ExtensionWorkspaceRecommendationsNotificationClassification = {
userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
};
const ignoreImportantExtensionRecommendationStorageKey = 'extensionsAssistant/importantRecommendationsIgnore';
const donotShowWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore';
const choiceNever = localize('neverShowAgain', "Don't Show Again");
type RecommendationsNotificationActions = {
onDidInstallRecommendedExtensions(extensions: IExtension[]): void;
onDidShowRecommendedExtensions(extensions: IExtension[]): void;
onDidCancelRecommendedExtensions(extensions: IExtension[]): void;
onDidNeverShowRecommendedExtensionsAgain(extensions: IExtension[]): void;
};
class RecommendationsNotification {
private _onDidClose = new Emitter<void>();
readonly onDidClose = this._onDidClose.event;
private _onDidChangeVisibility = new Emitter<boolean>();
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
private notificationHandle: INotificationHandle | undefined;
private cancelled: boolean = false;
constructor(
private readonly severity: Severity,
private readonly message: string,
private readonly choices: IPromptChoice[],
private readonly notificationService: INotificationService
) { }
show(): void {
if (!this.notificationHandle) {
this.updateNotificationHandle(this.notificationService.prompt(this.severity, this.message, this.choices, { sticky: true, onCancel: () => this.cancelled = true }));
}
}
hide(): void {
if (this.notificationHandle) {
this.onDidCloseDisposable.clear();
this.notificationHandle.close();
this.cancelled = false;
this.updateNotificationHandle(this.notificationService.prompt(this.severity, this.message, this.choices, { silent: true, sticky: false, onCancel: () => this.cancelled = true }));
}
}
isCancelled(): boolean {
return this.cancelled;
}
private onDidCloseDisposable = new MutableDisposable();
private onDidChangeVisibilityDisposable = new MutableDisposable();
private updateNotificationHandle(notificationHandle: INotificationHandle) {
this.onDidCloseDisposable.clear();
this.onDidChangeVisibilityDisposable.clear();
this.notificationHandle = notificationHandle;
this.onDidCloseDisposable.value = this.notificationHandle.onDidClose(() => {
this.onDidCloseDisposable.dispose();
this.onDidChangeVisibilityDisposable.dispose();
this._onDidClose.fire();
this._onDidClose.dispose();
this._onDidChangeVisibility.dispose();
});
this.onDidChangeVisibilityDisposable.value = this.notificationHandle.onDidChangeVisibility((e) => this._onDidChangeVisibility.fire(e));
}
}
type PendingRecommendationsNotification = { recommendationsNotification: RecommendationsNotification, source: RecommendationSource, token: CancellationToken };
type VisibleRecommendationsNotification = { recommendationsNotification: RecommendationsNotification, source: RecommendationSource, from: number };
export class ExtensionRecommendationNotificationService implements IExtensionRecommendationNotificationService {
declare readonly _serviceBrand: undefined;
private readonly tasExperimentService: ITASExperimentService | undefined;
// Ignored Important Recommendations
get ignoredRecommendations(): string[] {
return distinct([...(<string[]>JSON.parse(this.storageService.get(ignoreImportantExtensionRecommendationStorageKey, StorageScope.GLOBAL, '[]')))].map(i => i.toLowerCase()));
}
private recommendedExtensions: string[] = [];
private recommendationSources: RecommendationSource[] = [];
private hideVisibleNotificationPromise: CancelablePromise<void> | undefined;
private visibleNotification: VisibleRecommendationsNotification | undefined;
private pendingNotificaitons: PendingRecommendationsNotification[] = [];
constructor(
@IConfigurationService private readonly configurationService: IConfigurationService,
@IStorageService private readonly storageService: IStorageService,
@INotificationService private readonly notificationService: INotificationService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IWorkbenchExtensioManagementService private readonly extensionManagementService: IWorkbenchExtensioManagementService,
@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,
@IExtensionIgnoredRecommendationsService private readonly extensionIgnoredRecommendationsService: IExtensionIgnoredRecommendationsService,
@IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService,
@IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
@optional(ITASExperimentService) tasExperimentService: ITASExperimentService,
) {
this.tasExperimentService = tasExperimentService;
}
hasToIgnoreRecommendationNotifications(): boolean {
const config = this.configurationService.getValue<{ ignoreRecommendations: boolean, showRecommendationsOnlyOnDemand?: boolean }>('extensions');
return config.ignoreRecommendations || !!config.showRecommendationsOnlyOnDemand;
}
async promptImportantExtensionsInstallNotification(extensionIds: string[], message: string, searchValue: string, source: RecommendationSource): Promise<RecommendationsNotificationResult> {
const ignoredRecommendations = [...this.extensionIgnoredRecommendationsService.ignoredRecommendations, ...this.ignoredRecommendations];
extensionIds = extensionIds.filter(id => !ignoredRecommendations.includes(id));
if (!extensionIds.length) {
return RecommendationsNotificationResult.Ignored;
}
return this.promptRecommendationsNotification(extensionIds, message, searchValue, source, {
onDidInstallRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId: extension.identifier.id })),
onDidShowRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId: extension.identifier.id })),
onDidCancelRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId: extension.identifier.id })),
onDidNeverShowRecommendedExtensionsAgain: (extensions: IExtension[]) => {
for (const extension of extensions) {
this.addToImportantRecommendationsIgnore(extension.identifier.id);
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: extension.identifier.id });
}
this.notificationService.prompt(
Severity.Info,
localize('ignoreExtensionRecommendations', "Do you want to ignore all extension recommendations?"),
[{
label: localize('ignoreAll', "Yes, Ignore All"),
run: () => this.setIgnoreRecommendationsConfig(true)
}, {
label: localize('no', "No"),
run: () => this.setIgnoreRecommendationsConfig(false)
}]
);
},
});
}
async promptWorkspaceRecommendations(recommendations: string[]): Promise<void> {
if (this.storageService.getBoolean(donotShowWorkspaceRecommendationsStorageKey, StorageScope.WORKSPACE, false)) {
return;
}
let installed = await this.extensionManagementService.getInstalled();
installed = installed.filter(l => this.extensionEnablementService.getEnablementState(l) !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind
recommendations = recommendations.filter(extensionId => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier)));
if (!recommendations.length) {
return;
}
const result = await this.promptRecommendationsNotification(recommendations, localize('workspaceRecommended', "Do you want to install the recommended extensions for this repository?"), '@recommended ', RecommendationSource.WORKSPACE, {
onDidInstallRecommendedExtensions: () => this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'install' }),
onDidShowRecommendedExtensions: () => this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'show' }),
onDidCancelRecommendedExtensions: () => this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'cancelled' }),
onDidNeverShowRecommendedExtensionsAgain: () => this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'neverShowAgain' }),
});
if (result === RecommendationsNotificationResult.Accepted) {
this.storageService.store(donotShowWorkspaceRecommendationsStorageKey, true, StorageScope.WORKSPACE, StorageTarget.USER);
}
}
private async promptRecommendationsNotification(extensionIds: string[], message: string, searchValue: string, source: RecommendationSource, recommendationsNotificationActions: RecommendationsNotificationActions): Promise<RecommendationsNotificationResult> {
if (this.hasToIgnoreRecommendationNotifications()) {
return RecommendationsNotificationResult.Ignored;
}
// Ignore exe recommendation if the window
// => has shown an exe based recommendation already
// => or has shown any two recommendations already
if (source === RecommendationSource.EXE && (this.recommendationSources.includes(RecommendationSource.EXE) || this.recommendationSources.length >= 2)) {
return RecommendationsNotificationResult.TooMany;
}
// Ignore exe recommendation if recommendations are already shown
if (source === RecommendationSource.EXE && extensionIds.every(id => this.recommendedExtensions.includes(id))) {
return RecommendationsNotificationResult.Ignored;
}
const extensions = await this.getInstallableExtensions(extensionIds);
if (!extensions.length) {
return RecommendationsNotificationResult.Ignored;
}
if (this.tasExperimentService && extensionIds.indexOf('ms-vscode-remote.remote-wsl') !== -1) {
await this.tasExperimentService.getTreatment<boolean>('wslpopupaa');
}
this.recommendedExtensions = distinct([...this.recommendedExtensions, ...extensionIds]);
return raceCancellablePromises([
this.showRecommendationsNotification(extensions, message, searchValue, source, recommendationsNotificationActions),
this.waitUntilRecommendationsAreInstalled(extensions)
]);
}
private showRecommendationsNotification(extensions: IExtension[], message: string, searchValue: string, source: RecommendationSource,
{ onDidInstallRecommendedExtensions, onDidShowRecommendedExtensions, onDidCancelRecommendedExtensions, onDidNeverShowRecommendedExtensionsAgain }: RecommendationsNotificationActions): CancelablePromise<RecommendationsNotificationResult> {
return createCancelablePromise<RecommendationsNotificationResult>(async token => {
let accepted = false;
const choices: (IPromptChoice | IPromptChoiceWithMenu)[] = [];
const installExtensions = async (isMachineScoped?: boolean) => {
this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue));
onDidInstallRecommendedExtensions(extensions);
await Promise.all([
Promise.all(extensions.map(extension => this.extensionsWorkbenchService.open(extension, { pinned: true }))),
this.extensionManagementService.installExtensions(extensions.map(e => e.gallery!), { isMachineScoped })
]);
};
choices.push({
label: localize('install', "Install"),
run: () => installExtensions(),
menu: this.userDataAutoSyncEnablementService.isEnabled() && this.userDataSyncResourceEnablementService.isResourceEnabled(SyncResource.Extensions) ? [{
label: localize('install and do no sync', "Install (Do not sync)"),
run: () => installExtensions(true)
}] : undefined,
});
choices.push(...[{
label: localize('show recommendations', "Show Recommendations"),
run: async () => {
onDidShowRecommendedExtensions(extensions);
for (const extension of extensions) {
this.extensionsWorkbenchService.open(extension, { pinned: true });
}
this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue));
}
}, {
label: choiceNever,
isSecondary: true,
run: () => {
onDidNeverShowRecommendedExtensionsAgain(extensions);
}
}]);
try {
accepted = await this.doShowRecommendationsNotification(Severity.Info, message, choices, source, token);
} catch (error) {
if (!isPromiseCanceledError(error)) {
throw error;
}
}
if (accepted) {
return RecommendationsNotificationResult.Accepted;
} else {
onDidCancelRecommendedExtensions(extensions);
return RecommendationsNotificationResult.Cancelled;
}
});
}
private waitUntilRecommendationsAreInstalled(extensions: IExtension[]): CancelablePromise<RecommendationsNotificationResult.Accepted> {
const installedExtensions: string[] = [];
const disposables = new DisposableStore();
return createCancelablePromise(async token => {
disposables.add(token.onCancellationRequested(e => disposables.dispose()));
return new Promise<RecommendationsNotificationResult.Accepted>((c, e) => {
disposables.add(this.extensionManagementService.onInstallExtension(e => {
installedExtensions.push(e.identifier.id.toLowerCase());
if (extensions.every(e => installedExtensions.includes(e.identifier.id.toLowerCase()))) {
c(RecommendationsNotificationResult.Accepted);
}
}));
});
});
}
/**
* Show recommendations in Queue
* At any time only one recommendation is shown
* If a new recommendation comes in
* => If no recommendation is visible, show it immediately
* => Otherwise, add to the pending queue
* => If it is not exe based and has higher or same priority as current, hide the current notification after showing it for 3s.
* => Otherwise wait until the current notification is hidden.
*/
private async doShowRecommendationsNotification(severity: Severity, message: string, choices: IPromptChoice[], source: RecommendationSource, token: CancellationToken): Promise<boolean> {
const disposables = new DisposableStore();
try {
this.recommendationSources.push(source);
const recommendationsNotification = new RecommendationsNotification(severity, message, choices, this.notificationService);
Event.once(Event.filter(recommendationsNotification.onDidChangeVisibility, e => !e))(() => this.showNextNotification());
if (this.visibleNotification) {
const index = this.pendingNotificaitons.length;
token.onCancellationRequested(() => this.pendingNotificaitons.splice(index, 1), disposables);
this.pendingNotificaitons.push({ recommendationsNotification, source, token });
if (source !== RecommendationSource.EXE && source <= this.visibleNotification!.source) {
this.hideVisibleNotification(3000);
}
} else {
this.visibleNotification = { recommendationsNotification, source, from: Date.now() };
recommendationsNotification.show();
}
await raceCancellation(Event.toPromise(recommendationsNotification.onDidClose), token);
return !recommendationsNotification.isCancelled();
} finally {
disposables.dispose();
}
}
private showNextNotification(): void {
const index = this.getNextPendingNotificationIndex();
const [nextNotificaiton] = index > -1 ? this.pendingNotificaitons.splice(index, 1) : [];
// Show the next notification after a delay of 500ms (after the current notification is dismissed)
timeout(nextNotificaiton ? 500 : 0)
.then(() => {
this.unsetVisibileNotification();
if (nextNotificaiton) {
this.visibleNotification = { recommendationsNotification: nextNotificaiton.recommendationsNotification, source: nextNotificaiton.source, from: Date.now() };
nextNotificaiton.recommendationsNotification.show();
}
});
}
/**
* Return the recent high priroity pending notification
*/
private getNextPendingNotificationIndex(): number {
let index = this.pendingNotificaitons.length - 1;
if (this.pendingNotificaitons.length) {
for (let i = 0; i < this.pendingNotificaitons.length; i++) {
if (this.pendingNotificaitons[i].source <= this.pendingNotificaitons[index].source) {
index = i;
}
}
}
return index;
}
private hideVisibleNotification(timeInMillis: number): void {
if (this.visibleNotification && !this.hideVisibleNotificationPromise) {
const visibleNotification = this.visibleNotification;
this.hideVisibleNotificationPromise = timeout(Math.max(timeInMillis - (Date.now() - visibleNotification.from), 0));
this.hideVisibleNotificationPromise.then(() => visibleNotification!.recommendationsNotification.hide());
}
}
private unsetVisibileNotification(): void {
this.hideVisibleNotificationPromise?.cancel();
this.hideVisibleNotificationPromise = undefined;
this.visibleNotification = undefined;
}
private async getInstallableExtensions(extensionIds: string[]): Promise<IExtension[]> {
const extensions: IExtension[] = [];
if (extensionIds.length) {
const pager = await this.extensionsWorkbenchService.queryGallery({ names: extensionIds, pageSize: extensionIds.length, source: 'install-recommendations' }, CancellationToken.None);
for (const extension of pager.firstPage) {
if (extension.gallery && (await this.extensionManagementService.canInstall(extension.gallery))) {
extensions.push(extension);
}
}
}
return extensions;
}
private async runAction(action: IAction): Promise<void> {
try {
await action.run();
} finally {
action.dispose();
}
}
private addToImportantRecommendationsIgnore(id: string) {
const importantRecommendationsIgnoreList = [...this.ignoredRecommendations];
if (!importantRecommendationsIgnoreList.includes(id.toLowerCase())) {
importantRecommendationsIgnoreList.push(id.toLowerCase());
this.storageService.store(ignoreImportantExtensionRecommendationStorageKey, JSON.stringify(importantRecommendationsIgnoreList), StorageScope.GLOBAL, StorageTarget.USER);
}
}
private setIgnoreRecommendationsConfig(configVal: boolean) {
this.configurationService.updateValue('extensions.ignoreRecommendations', configVal);
}
}

View File

@@ -4,16 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { IExtensionRecommendationReason, EnablementState, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { localize } from 'vs/nls';
import { SearchExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { EnablementState, ExtensionRecommendationSource, IExtensionRecommendationReson, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionsConfiguration, ConfigurationKey, IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { IAction } from 'vs/base/common/actions';
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { CancellationToken } from 'vs/base/common/cancellation';
@@ -31,11 +30,9 @@ type ExtensionWorkspaceRecommendationsNotificationClassification = {
const ignoreWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore';
const ignoreImportantExtensionRecommendation = 'extensionsAssistant/importantRecommendationsIgnore';
const choiceNever = localize('neverShowAgain', "Don't Show Again");
export type ExtensionRecommendation = {
readonly extensionId: string,
readonly source: ExtensionRecommendationSource;
readonly reason: IExtensionRecommendationReson;
readonly reason: IExtensionRecommendationReason;
};
export abstract class ExtensionRecommendations extends Disposable {
@@ -44,7 +41,8 @@ export abstract class ExtensionRecommendations extends Disposable {
protected abstract doActivate(): Promise<void>;
constructor(
protected readonly promptedExtensionRecommendations: PromptedExtensionRecommendations,
// {{SQL CARBON EDIT}}
protected readonly promptedExtensionRecommendations?: PromptedExtensionRecommendations,
) {
super();
}
@@ -72,10 +70,8 @@ export class PromptedExtensionRecommendations extends Disposable {
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
) {
super();
storageKeysSyncRegistryService.registerStorageKey({ key: ignoreImportantExtensionRecommendation, version: 1 });
}
async promptImportantExtensionsInstallNotification(extensionIds: string[], message: string, searchValue: string): Promise<void> {
@@ -182,7 +178,7 @@ export class PromptedExtensionRecommendations extends Disposable {
isSecondary: true,
run: () => {
this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'neverShowAgain' });
this.storageService.store(ignoreWorkspaceRecommendationsStorageKey, true, StorageScope.WORKSPACE);
this.storageService.store(ignoreWorkspaceRecommendationsStorageKey, true, StorageScope.WORKSPACE, StorageTarget.MACHINE);
}
}],
{
@@ -240,7 +236,7 @@ export class PromptedExtensionRecommendations extends Disposable {
private addToImportantRecommendationsIgnore(id: string) {
const importantRecommendationsIgnoreList = <string[]>JSON.parse(this.storageService.get(ignoreImportantExtensionRecommendation, StorageScope.GLOBAL, '[]'));
importantRecommendationsIgnoreList.push(id.toLowerCase());
this.storageService.store(ignoreImportantExtensionRecommendation, JSON.stringify(importantRecommendationsIgnoreList), StorageScope.GLOBAL);
this.storageService.store(ignoreImportantExtensionRecommendation, JSON.stringify(importantRecommendationsIgnoreList), StorageScope.GLOBAL, StorageTarget.MACHINE);
}
private setIgnoreRecommendationsConfig(configVal: boolean) {

View File

@@ -5,42 +5,35 @@
import { Disposable } from 'vs/base/common/lifecycle';
import { IExtensionManagementService, IExtensionGalleryService, InstallOperation, DidInstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionRecommendationsService, ExtensionRecommendationReason, RecommendationChangeNotification, IExtensionRecommendation, ExtensionRecommendationSource } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage';
import { IExtensionRecommendationsService, ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ShowRecommendationsOnlyOnDemandKey } from 'vs/workbench/contrib/extensions/common/extensions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { distinct, shuffle } from 'vs/base/common/arrays';
import { Emitter, Event } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { LifecyclePhase, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { LifecyclePhase, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { DynamicWorkspaceRecommendations } from 'vs/workbench/contrib/extensions/browser/dynamicWorkspaceRecommendations';
import { ExeBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/exeBasedRecommendations';
import { ExperimentalRecommendations } from 'vs/workbench/contrib/extensions/browser/experimentalRecommendations';
import { WorkspaceRecommendations } from 'vs/workbench/contrib/extensions/browser/workspaceRecommendations';
import { FileBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/fileBasedRecommendations';
import { KeymapRecommendations } from 'vs/workbench/contrib/extensions/browser/keymapRecommendations';
import { ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { ConfigBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/configBasedRecommendations';
import { StaticRecommendations } from 'sql/workbench/contrib/extensions/browser/staticRecommendations';
import { ScenarioRecommendations } from 'sql/workbench/contrib/extensions/browser/scenarioRecommendations';
import { ExtensionsPolicyKey, ExtensionsPolicy } from 'vs/platform/extensions/common/extensions';
import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
import { IExtensionRecommendation } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; // {{ SQL CARBON EDIT }}
type IgnoreRecommendationClassification = {
recommendationReason: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
};
const ignoredRecommendationsStorageKey = 'extensionsAssistant/ignored_recommendations';
export class ExtensionRecommendationsService extends Disposable implements IExtensionRecommendationsService {
declare readonly _serviceBrand: undefined;
private readonly promptedExtensionRecommendations: PromptedExtensionRecommendations;
// Recommendations
private readonly fileBasedRecommendations: FileBasedRecommendations;
private readonly workspaceRecommendations: WorkspaceRecommendations;
@@ -52,41 +45,33 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
private readonly staticRecommendations: StaticRecommendations; // {{SQL CARBON EDIT}} add ours
private readonly scenarioRecommendations: ScenarioRecommendations; // {{SQL CARBON EDIT}} add ours
// Ignored Recommendations
private globallyIgnoredRecommendations: string[] = [];
public readonly activationPromise: Promise<void>;
private sessionSeed: number;
private readonly _onRecommendationChange = this._register(new Emitter<RecommendationChangeNotification>());
onRecommendationChange: Event<RecommendationChangeNotification> = this._onRecommendationChange.event;
private _onDidChangeRecommendations = this._register(new Emitter<void>());
readonly onDidChangeRecommendations = this._onDidChangeRecommendations.event;
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@ILifecycleService private readonly lifecycleService: ILifecycleService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
@IStorageService private readonly storageService: IStorageService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IExtensionIgnoredRecommendationsService private readonly extensionRecommendationsManagementService: IExtensionIgnoredRecommendationsService,
@IExtensionRecommendationNotificationService private readonly extensionRecommendationNotificationService: IExtensionRecommendationNotificationService,
) {
super();
storageKeysSyncRegistryService.registerStorageKey({ key: ignoredRecommendationsStorageKey, version: 1 });
const isExtensionAllowedToBeRecommended = (extensionId: string) => this.isExtensionAllowedToBeRecommended(extensionId);
this.promptedExtensionRecommendations = instantiationService.createInstance(PromptedExtensionRecommendations, isExtensionAllowedToBeRecommended);
this.workspaceRecommendations = instantiationService.createInstance(WorkspaceRecommendations, this.promptedExtensionRecommendations);
this.fileBasedRecommendations = instantiationService.createInstance(FileBasedRecommendations, this.promptedExtensionRecommendations);
this.experimentalRecommendations = instantiationService.createInstance(ExperimentalRecommendations, this.promptedExtensionRecommendations);
this.configBasedRecommendations = instantiationService.createInstance(ConfigBasedRecommendations, this.promptedExtensionRecommendations);
this.exeBasedRecommendations = instantiationService.createInstance(ExeBasedRecommendations, this.promptedExtensionRecommendations);
this.dynamicWorkspaceRecommendations = instantiationService.createInstance(DynamicWorkspaceRecommendations, this.promptedExtensionRecommendations);
this.keymapRecommendations = instantiationService.createInstance(KeymapRecommendations, this.promptedExtensionRecommendations);
this.staticRecommendations = instantiationService.createInstance(StaticRecommendations, this.promptedExtensionRecommendations); // {{SQL CARBON EDIT}} add ours
this.scenarioRecommendations = instantiationService.createInstance(ScenarioRecommendations, this.promptedExtensionRecommendations); // {{SQL CARBON EDIT}} add ours
this.workspaceRecommendations = instantiationService.createInstance(WorkspaceRecommendations);
this.fileBasedRecommendations = instantiationService.createInstance(FileBasedRecommendations);
this.experimentalRecommendations = instantiationService.createInstance(ExperimentalRecommendations);
this.configBasedRecommendations = instantiationService.createInstance(ConfigBasedRecommendations);
this.exeBasedRecommendations = instantiationService.createInstance(ExeBasedRecommendations);
this.dynamicWorkspaceRecommendations = instantiationService.createInstance(DynamicWorkspaceRecommendations);
this.keymapRecommendations = instantiationService.createInstance(KeymapRecommendations);
this.staticRecommendations = instantiationService.createInstance(StaticRecommendations); // {{SQL CARBON EDIT}} add ours
this.scenarioRecommendations = instantiationService.createInstance(ScenarioRecommendations); // {{SQL CARBON EDIT}} add ours
if (!this.isEnabled()) {
this.sessionSeed = 0;
@@ -95,13 +80,11 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
}
this.sessionSeed = +new Date();
this.globallyIgnoredRecommendations = this.getCachedIgnoredRecommendations();
// Activation
this.activationPromise = this.activate();
this._register(this.extensionManagementService.onDidInstallExtension(e => this.onDidInstallExtension(e)));
this._register(this.storageService.onDidChangeStorage(e => this.onDidStorageChange(e)));
}
private async activate(): Promise<void> {
@@ -115,20 +98,23 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
this.keymapRecommendations.activate(),
this.staticRecommendations.activate(), // {{SQL CARBON EDIT}} add ours
this.scenarioRecommendations.activate(), // {{SQL CARBON EDIT}} add ours
this.lifecycleService.when(LifecyclePhase.Eventually)
.then(async () => {
if (!this.configurationService.getValue<boolean>(ShowRecommendationsOnlyOnDemandKey)) {
await this.activateProactiveRecommendations();
}
})
]);
this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations, this.extensionRecommendationsManagementService.onDidChangeIgnoredRecommendations)(() => this._onDidChangeRecommendations.fire()));
this._register(this.extensionRecommendationsManagementService.onDidChangeGlobalIgnoredRecommendation(({ extensionId, isRecommended }) => {
if (!isRecommended) {
const reason = this.getAllRecommendationsWithReason()[extensionId];
if (reason && reason.reasonId) {
this.telemetryService.publicLog2<{ extensionId: string, recommendationReason: ExtensionRecommendationReason }, IgnoreRecommendationClassification>('extensionsRecommendations:ignoreRecommendation', { extensionId, recommendationReason: reason.reasonId });
}
}
}));
await this.promptWorkspaceRecommendations();
this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations)(() => this.promptWorkspaceRecommendations()));
}
private isEnabled(): boolean {
return this.galleryService.isEnabled() && !this.environmentService.extensionDevelopmentLocationURI && this.configurationService.getValue<string>(ExtensionsPolicyKey) !== ExtensionsPolicy.allowNone;
return this.galleryService.isEnabled() && !this.environmentService.extensionDevelopmentLocationURI;
}
private async activateProactiveRecommendations(): Promise<void> {
@@ -161,7 +147,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
return output;
}
async getConfigBasedRecommendations(): Promise<{ important: IExtensionRecommendation[], others: IExtensionRecommendation[] }> {
async getConfigBasedRecommendations(): Promise<{ important: string[], others: string[] }> {
await this.configBasedRecommendations.activate();
return {
important: this.toExtensionRecommendations(this.configBasedRecommendations.importantRecommendations),
@@ -169,7 +155,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
};
}
async getOtherRecommendations(): Promise<IExtensionRecommendation[]> {
async getOtherRecommendations(): Promise<string[]> {
await this.activateProactiveRecommendations();
const recommendations = [
@@ -185,13 +171,10 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
shuffle(extensionIds, this.sessionSeed);
return extensionIds.map(extensionId => {
const sources: ExtensionRecommendationSource[] = distinct(recommendations.filter(r => r.extensionId === extensionId).map(r => r.source));
return (<IExtensionRecommendation>{ extensionId, sources });
});
return extensionIds;
}
async getImportantRecommendations(): Promise<IExtensionRecommendation[]> {
async getImportantRecommendations(): Promise<string[]> {
await this.activateProactiveRecommendations();
const recommendations = [
@@ -205,17 +188,14 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
shuffle(extensionIds, this.sessionSeed);
return extensionIds.map(extensionId => {
const sources: ExtensionRecommendationSource[] = distinct(recommendations.filter(r => r.extensionId === extensionId).map(r => r.source));
return (<IExtensionRecommendation>{ extensionId, sources });
});
return extensionIds;
}
getKeymapRecommendations(): IExtensionRecommendation[] {
getKeymapRecommendations(): string[] {
return this.toExtensionRecommendations(this.keymapRecommendations.recommendations);
}
async getWorkspaceRecommendations(): Promise<IExtensionRecommendation[]> {
async getWorkspaceRecommendations(): Promise<string[]> {
if (!this.isEnabled()) {
return [];
}
@@ -223,40 +203,17 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
return this.toExtensionRecommendations(this.workspaceRecommendations.recommendations);
}
async getExeBasedRecommendations(exe?: string): Promise<{ important: IExtensionRecommendation[], others: IExtensionRecommendation[] }> {
async getExeBasedRecommendations(exe?: string): Promise<{ important: string[], others: string[] }> {
await this.exeBasedRecommendations.activate();
const { important, others } = exe ? this.exeBasedRecommendations.getRecommendations(exe)
: { important: this.exeBasedRecommendations.importantRecommendations, others: this.exeBasedRecommendations.otherRecommendations };
return { important: this.toExtensionRecommendations(important), others: this.toExtensionRecommendations(others) };
}
getFileBasedRecommendations(): IExtensionRecommendation[] {
getFileBasedRecommendations(): string[] {
return this.toExtensionRecommendations(this.fileBasedRecommendations.recommendations);
}
getIgnoredRecommendations(): ReadonlyArray<string> {
return this.globallyIgnoredRecommendations;
}
toggleIgnoredRecommendation(extensionId: string, shouldIgnore: boolean) {
extensionId = extensionId.toLowerCase();
const ignored = this.globallyIgnoredRecommendations.indexOf(extensionId) !== -1;
if (ignored === shouldIgnore) {
return;
}
if (shouldIgnore) {
const reason = this.getAllRecommendationsWithReason()[extensionId];
if (reason && reason.reasonId) {
this.telemetryService.publicLog2<{ extensionId: string, recommendationReason: ExtensionRecommendationReason }, IgnoreRecommendationClassification>('extensionsRecommendations:ignoreRecommendation', { extensionId, recommendationReason: reason.reasonId });
}
}
this.globallyIgnoredRecommendations = shouldIgnore ? [...this.globallyIgnoredRecommendations, extensionId] : this.globallyIgnoredRecommendations.filter(id => id !== extensionId);
this.storeCachedIgnoredRecommendations(this.globallyIgnoredRecommendations);
this._onRecommendationChange.fire({ extensionId, isRecommended: !shouldIgnore });
}
private onDidInstallExtension(e: DidInstallExtensionEvent): void {
if (e.gallery && e.operation === InstallOperation.Install) {
const extRecommendations = this.getAllRecommendationsWithReason() || {};
@@ -275,22 +232,15 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
}
}
private toExtensionRecommendations(recommendations: ReadonlyArray<ExtensionRecommendation>): IExtensionRecommendation[] {
private toExtensionRecommendations(recommendations: ReadonlyArray<ExtensionRecommendation>): string[] {
const extensionIds = distinct(recommendations.map(e => e.extensionId))
.filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId));
return extensionIds.map(extensionId => {
const sources: ExtensionRecommendationSource[] = distinct(recommendations.filter(r => r.extensionId === extensionId).map(r => r.source));
return (<IExtensionRecommendation>{ extensionId, sources });
});
return extensionIds;
}
private isExtensionAllowedToBeRecommended(id: string): boolean {
const allIgnoredRecommendations = [
...this.globallyIgnoredRecommendations,
...this.workspaceRecommendations.ignoredRecommendations
];
return allIgnoredRecommendations.indexOf(id.toLowerCase()) === -1;
private isExtensionAllowedToBeRecommended(extensionId: string): boolean {
return !this.extensionRecommendationsManagementService.ignoredRecommendations.includes(extensionId.toLowerCase());
}
private async promptWorkspaceRecommendations(): Promise<void> {
@@ -299,42 +249,11 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
.filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId));
if (allowedRecommendations.length) {
await this.promptedExtensionRecommendations.promptWorkspaceRecommendations(allowedRecommendations);
await this.extensionRecommendationNotificationService.promptWorkspaceRecommendations(allowedRecommendations);
}
}
private onDidStorageChange(e: IWorkspaceStorageChangeEvent): void {
if (e.key === ignoredRecommendationsStorageKey && e.scope === StorageScope.GLOBAL
&& this.ignoredRecommendationsValue !== this.getStoredIgnoredRecommendationsValue() /* This checks if current window changed the value or not */) {
this._ignoredRecommendationsValue = undefined;
this.globallyIgnoredRecommendations = this.getCachedIgnoredRecommendations();
}
}
private getCachedIgnoredRecommendations(): string[] {
const ignoredRecommendations: string[] = JSON.parse(this.ignoredRecommendationsValue);
return ignoredRecommendations.map(e => e.toLowerCase());
}
private storeCachedIgnoredRecommendations(ignoredRecommendations: string[]): void {
this.ignoredRecommendationsValue = JSON.stringify(ignoredRecommendations);
}
private _ignoredRecommendationsValue: string | undefined;
private get ignoredRecommendationsValue(): string {
if (!this._ignoredRecommendationsValue) {
this._ignoredRecommendationsValue = this.getStoredIgnoredRecommendationsValue();
}
return this._ignoredRecommendationsValue;
}
private set ignoredRecommendationsValue(ignoredRecommendationsValue: string) {
if (this.ignoredRecommendationsValue !== ignoredRecommendationsValue) {
this._ignoredRecommendationsValue = ignoredRecommendationsValue;
this.setStoredIgnoredRecommendationsValue(ignoredRecommendationsValue);
}
}
// {{SQL CARBON EDIT}}
promptRecommendedExtensionsByScenario(scenarioType: string): void {
@@ -346,12 +265,4 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
}
// {{SQL CARBON EDIT}} - End
private getStoredIgnoredRecommendationsValue(): string {
return this.storageService.get(ignoredRecommendationsStorageKey, StorageScope.GLOBAL, '[]');
}
private setStoredIgnoredRecommendationsValue(value: string): void {
this.storageService.store(ignoredRecommendationsStorageKey, value, StorageScope.GLOBAL);
}
}

View File

@@ -3,9 +3,15 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService';
import { localize } from 'vs/nls';
import { Registry } from 'vs/platform/registry/common/platform';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { EditorDescriptor, IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
import { RuntimeExtensionsEditor } from 'vs/workbench/contrib/extensions/browser/browserRuntimeExtensionsEditor';
import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput';
// TODO@sandbox TODO@ben move back into common/extensions.contribution.ts when 'semver-umd' can be loaded
registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService);
// Running Extensions
Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
EditorDescriptor.create(RuntimeExtensionsEditor, RuntimeExtensionsEditor.ID, localize('runtimeExtension', "Running Extensions")),
[new SyncDescriptor(RuntimeExtensionsInput)]
);

View File

@@ -0,0 +1,33 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Codicon } from 'vs/base/common/codicons';
import { localize } from 'vs/nls';
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
export const extensionsViewIcon = registerIcon('extensions-view-icon', Codicon.extensions, localize('extensionsViewIcon', 'View icon of the extensions view.'));
export const manageExtensionIcon = registerIcon('extensions-manage', Codicon.gear, localize('manageExtensionIcon', 'Icon for the \'Manage\' action in the extensions view.'));
export const clearSearchResultsIcon = registerIcon('extensions-clear-search-results', Codicon.clearAll, localize('clearSearchResultsIcon', 'Icon for the \'Clear Search Result\' action in the extensions view.'));
export const refreshIcon = registerIcon('extensions-refresh', Codicon.refresh, localize('refreshIcon', 'Icon for the \'Refresh\' action in the extensions view.'));
export const filterIcon = registerIcon('extensions-filter', Codicon.filter, localize('filterIcon', 'Icon for the \'Filter\' action in the extensions view.'));
export const installLocalInRemoteIcon = registerIcon('extensions-install-local-in-remote', Codicon.cloudDownload, localize('installLocalInRemoteIcon', 'Icon for the \'Install Local Extension in Remote\' action in the extensions view.'));
export const installWorkspaceRecommendedIcon = registerIcon('extensions-install-workspace-recommended', Codicon.cloudDownload, localize('installWorkspaceRecommendedIcon', 'Icon for the \'Install Workspace Recommended Extensions\' action in the extensions view.'));
export const configureRecommendedIcon = registerIcon('extensions-configure-recommended', Codicon.pencil, localize('configureRecommendedIcon', 'Icon for the \'Configure Recommended Extensions\' action in the extensions view.'));
export const syncEnabledIcon = registerIcon('extensions-sync-enabled', Codicon.sync, localize('syncEnabledIcon', 'Icon to indicate that an extension is synced.'));
export const syncIgnoredIcon = registerIcon('extensions-sync-ignored', Codicon.syncIgnored, localize('syncIgnoredIcon', 'Icon to indicate that an extension is ignored when syncing.'));
export const remoteIcon = registerIcon('extensions-remote', Codicon.remote, localize('remoteIcon', 'Icon to indicate that an extension is remote in the extensions view and editor.'));
export const installCountIcon = registerIcon('extensions-install-count', Codicon.cloudDownload, localize('installCountIcon', 'Icon shown along with the install count in the extensions view and editor.'));
export const ratingIcon = registerIcon('extensions-rating', Codicon.star, localize('ratingIcon', 'Icon shown along with the rating in the extensions view and editor.'));
export const starFullIcon = registerIcon('extensions-star-full', Codicon.starFull, localize('starFullIcon', 'Full star icon used for the rating in the extensions editor.'));
export const starHalfIcon = registerIcon('extensions-star-half', Codicon.starHalf, localize('starHalfIcon', 'Half star icon used for the rating in the extensions editor.'));
export const starEmptyIcon = registerIcon('extensions-star-empty', Codicon.starEmpty, localize('starEmptyIcon', 'Empty star icon used for the rating in the extensions editor.'));
export const warningIcon = registerIcon('extensions-warning-message', Codicon.warning, localize('warningIcon', 'Icon shown with a warning message in the extensions editor.'));
export const infoIcon = registerIcon('extensions-info-message', Codicon.info, localize('infoIcon', 'Icon shown with an info message in the extensions editor.'));

View File

@@ -14,9 +14,9 @@ import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging';
import { Event } from 'vs/base/common/event';
import { domEvent } from 'vs/base/browser/event';
import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { InstallAction, UpdateAction, ManageExtensionAction, ReloadAction, MaliciousStatusLabelAction, ExtensionActionViewItem, StatusLabelAction, RemoteInstallAction, SystemDisabledWarningAction, ExtensionToolTipAction, LocalInstallAction, SyncIgnoredIconAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { UpdateAction, ManageExtensionAction, ReloadAction, MaliciousStatusLabelAction, ExtensionActionViewItem, StatusLabelAction, RemoteInstallAction, SystemDisabledWarningAction, ExtensionToolTipAction, LocalInstallAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { Label, RatingsWidget, /*InstallCountWidget,*/ RecommendationWidget, RemoteBadgeWidget, TooltipWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets';
import { Label, RatingsWidget, /*InstallCountWidget,*/ RecommendationWidget, RemoteBadgeWidget, TooltipWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets';
import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions';
import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { INotificationService } from 'vs/platform/notification/common/notification';
@@ -24,6 +24,9 @@ import { isLanguagePackExtension } from 'vs/platform/extensions/common/extension
import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
import { foreground, listActiveSelectionForeground, listActiveSelectionBackground, listInactiveSelectionForeground, listInactiveSelectionBackground, listFocusForeground, listFocusBackground, listHoverForeground, listHoverBackground } from 'vs/platform/theme/common/colorRegistry';
import { WORKBENCH_BACKGROUND } from 'vs/workbench/common/theme';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
export const EXTENSION_LIST_ELEMENT_HEIGHT = 62;
export interface IExtensionsViewState {
onFocus: Event<IExtension>;
@@ -47,7 +50,7 @@ export interface ITemplateData {
}
export class Delegate implements IListVirtualDelegate<IExtension> {
getHeight() { return 62; }
getHeight() { return EXTENSION_LIST_ELEMENT_HEIGHT; }
getTemplateId() { return 'extension'; }
}
@@ -62,6 +65,7 @@ export class Renderer implements IPagedRenderer<IExtension, ITemplateData> {
@IExtensionService private readonly extensionService: IExtensionService,
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IContextMenuService private readonly contextMenuService: IContextMenuService,
) { }
get templateId() { return 'extension'; }
@@ -80,6 +84,7 @@ export class Renderer implements IPagedRenderer<IExtension, ITemplateData> {
const version = append(header, $('span.version'));
// const installCount = append(header, $('span.install-count')); {{SQL CARBON EDIT}} no unused
const ratings = append(header, $('span.ratings'));
const syncIgnore = append(header, $('span.sync-ignored'));
const headerRemoteBadgeWidget = this.instantiationService.createInstance(RemoteBadgeWidget, header, false);
const description = append(details, $('.description.ellipsis'));
const footer = append(details, $('.footer'));
@@ -87,8 +92,11 @@ export class Renderer implements IPagedRenderer<IExtension, ITemplateData> {
const actionbar = new ActionBar(footer, {
animated: false,
actionViewItemProvider: (action: IAction) => {
if (action.id === ManageExtensionAction.ID) {
return (<ManageExtensionAction>action).createActionViewItem();
if (action instanceof ActionWithDropDownAction) {
return new ExtensionActionWithDropdownActionViewItem(action, { icon: true, label: true, menuActionsOrProvider: { getActions: () => action.menuActions }, menuActionClassNames: (action.class || '').split(' ') }, this.contextMenuService);
}
if (action instanceof ExtensionDropDownAction) {
return action.createActionViewItem();
}
return new ExtensionActionViewItem(null, action, actionOptions);
}
@@ -99,10 +107,10 @@ export class Renderer implements IPagedRenderer<IExtension, ITemplateData> {
const reloadAction = this.instantiationService.createInstance(ReloadAction);
const actions = [
this.instantiationService.createInstance(StatusLabelAction),
this.instantiationService.createInstance(SyncIgnoredIconAction),
this.instantiationService.createInstance(UpdateAction),
reloadAction,
this.instantiationService.createInstance(InstallAction),
this.instantiationService.createInstance(InstallDropdownAction),
this.instantiationService.createInstance(InstallingLabelAction),
this.instantiationService.createInstance(RemoteInstallAction, false),
this.instantiationService.createInstance(LocalInstallAction),
this.instantiationService.createInstance(MaliciousStatusLabelAction, false),
@@ -118,6 +126,7 @@ export class Renderer implements IPagedRenderer<IExtension, ITemplateData> {
headerRemoteBadgeWidget,
tooltipWidget,
this.instantiationService.createInstance(Label, version, (e: IExtension) => e.version),
this.instantiationService.createInstance(SyncIgnoredWidget, syncIgnore),
// {{SQL CARBON EDIT}}
// this.instantiationService.createInstance(InstallCountWidget, installCount, true),
this.instantiationService.createInstance(RatingsWidget, ratings, true)
@@ -201,19 +210,31 @@ export class Renderer implements IPagedRenderer<IExtension, ITemplateData> {
this.extensionViewState.onFocus(e => {
if (areSameExtensions(extension.identifier, e.identifier)) {
data.actionbar.viewItems.forEach(item => (<ExtensionActionViewItem>item).setFocus(true));
data.actionbar.viewItems.forEach(item => {
if (item instanceof ExtensionActionViewItem || item instanceof ExtensionActionWithDropdownActionViewItem) {
item.setFocus(true);
}
});
}
}, this, data.extensionDisposables);
this.extensionViewState.onBlur(e => {
if (areSameExtensions(extension.identifier, e.identifier)) {
data.actionbar.viewItems.forEach(item => (<ExtensionActionViewItem>item).setFocus(false));
data.actionbar.viewItems.forEach(item => {
if (item instanceof ExtensionActionViewItem || item instanceof ExtensionActionWithDropdownActionViewItem) {
item.setFocus(false);
}
});
}
}, this, data.extensionDisposables);
}
disposeElement(extension: IExtension, index: number, data: ITemplateData): void {
data.extensionDisposables = dispose(data.extensionDisposables);
}
disposeTemplate(data: ITemplateData): void {
data.extensionDisposables = dispose(data.extensionDisposables);
data.disposables = dispose(data.disposables);
}
}
@@ -228,38 +249,40 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) =
}
const listActiveSelectionForegroundColor = theme.getColor(listActiveSelectionForeground);
const listActiveSelectionBackgroundColor = theme.getColor(listActiveSelectionBackground);
if (listActiveSelectionForegroundColor && listActiveSelectionBackgroundColor) {
const authorForeground = listActiveSelectionForegroundColor.transparent(.9).makeOpaque(listActiveSelectionBackgroundColor);
if (listActiveSelectionForegroundColor) {
const backgroundColor = theme.getColor(listActiveSelectionBackground) || WORKBENCH_BACKGROUND(theme);
const authorForeground = listActiveSelectionForegroundColor.transparent(.9).makeOpaque(backgroundColor);
collector.addRule(`.extensions-list .monaco-list:focus .monaco-list-row:not(.disabled).focused.selected .author { color: ${authorForeground}; }`);
collector.addRule(`.extensions-list .monaco-list:focus .monaco-list-row:not(.disabled).selected .author { color: ${authorForeground}; }`);
const disabledExtensionForeground = listActiveSelectionForegroundColor.transparent(.5).makeOpaque(listActiveSelectionBackgroundColor);
const disabledExtensionForeground = listActiveSelectionForegroundColor.transparent(.5).makeOpaque(backgroundColor);
collector.addRule(`.extensions-list .monaco-list:focus .monaco-list-row.disabled.focused.selected { color: ${disabledExtensionForeground}; }`);
collector.addRule(`.extensions-list .monaco-list:focus .monaco-list-row.disabled.selected { color: ${disabledExtensionForeground}; }`);
}
const listInactiveSelectionForegroundColor = theme.getColor(listInactiveSelectionForeground);
const listInactiveSelectionBackgroundColor = theme.getColor(listInactiveSelectionBackground);
if (listInactiveSelectionForegroundColor && listInactiveSelectionBackgroundColor) {
const authorForeground = listInactiveSelectionForegroundColor.transparent(.9).makeOpaque(listInactiveSelectionBackgroundColor);
if (listInactiveSelectionForegroundColor) {
const backgroundColor = theme.getColor(listInactiveSelectionBackground) || WORKBENCH_BACKGROUND(theme);
const authorForeground = listInactiveSelectionForegroundColor.transparent(.9).makeOpaque(backgroundColor);
collector.addRule(`.extensions-list .monaco-list .monaco-list-row:not(.disabled).selected .author { color: ${authorForeground}; }`);
const disabledExtensionForeground = listInactiveSelectionForegroundColor.transparent(.5).makeOpaque(listInactiveSelectionBackgroundColor);
const disabledExtensionForeground = listInactiveSelectionForegroundColor.transparent(.5).makeOpaque(backgroundColor);
collector.addRule(`.extensions-list .monaco-list .monaco-list-row.disabled.selected { color: ${disabledExtensionForeground}; }`);
}
const listFocusForegroundColor = theme.getColor(listFocusForeground);
const listFocusBackgroundColor = theme.getColor(listFocusBackground);
if (listFocusForegroundColor && listFocusBackgroundColor) {
const authorForeground = listFocusForegroundColor.transparent(.9).makeOpaque(listFocusBackgroundColor);
if (listFocusForegroundColor) {
const backgroundColor = theme.getColor(listFocusBackground) || WORKBENCH_BACKGROUND(theme);
const authorForeground = listFocusForegroundColor.transparent(.9).makeOpaque(backgroundColor);
collector.addRule(`.extensions-list .monaco-list:focus .monaco-list-row:not(.disabled).focused .author { color: ${authorForeground}; }`);
const disabledExtensionForeground = listFocusForegroundColor.transparent(.5).makeOpaque(listFocusBackgroundColor);
const disabledExtensionForeground = listFocusForegroundColor.transparent(.5).makeOpaque(backgroundColor);
collector.addRule(`.extensions-list .monaco-list:focus .monaco-list-row.disabled.focused { color: ${disabledExtensionForeground}; }`);
}
const listHoverForegroundColor = theme.getColor(listHoverForeground);
const listHoverBackgroundColor = theme.getColor(listHoverBackground);
if (listHoverForegroundColor && listHoverBackgroundColor) {
const authorForeground = listHoverForegroundColor.transparent(.9).makeOpaque(listHoverBackgroundColor);
if (listHoverForegroundColor) {
const backgroundColor = theme.getColor(listHoverBackground) || WORKBENCH_BACKGROUND(theme);
const authorForeground = listHoverForegroundColor.transparent(.9).makeOpaque(backgroundColor);
collector.addRule(`.extensions-list .monaco-list .monaco-list-row:hover:not(.disabled):not(.selected):.not(.focused) .author { color: ${authorForeground}; }`);
const disabledExtensionForeground = listHoverForegroundColor.transparent(.5).makeOpaque(listHoverBackgroundColor);
const disabledExtensionForeground = listHoverForegroundColor.transparent(.5).makeOpaque(backgroundColor);
collector.addRule(`.extensions-list .monaco-list .monaco-list-row.disabled:hover:not(.selected):.not(.focused) { color: ${disabledExtensionForeground}; }`);
}
});

View File

@@ -9,7 +9,7 @@ import { timeout, Delayer } from 'vs/base/common/async';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { Event as EventOf, Emitter } from 'vs/base/common/event';
import { Event, Emitter } from 'vs/base/common/event';
import { IAction, Action, Separator, SubmenuAction } from 'vs/base/common/actions';
import { IViewlet } from 'vs/workbench/common/viewlet';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
@@ -17,27 +17,27 @@ import { append, $, Dimension, hide, show } from 'vs/base/browser/dom';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, AutoUpdateConfigurationKey, CloseExtensionDetailsOnViewChangeKey } from '../common/extensions';
import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, AutoUpdateConfigurationKey, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID } from '../common/extensions';
import {
ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction, CheckForUpdatesAction, DisableAllAction, EnableAllAction,
EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction, SearchCategoryAction,
/*RecentlyPublishedExtensionsAction, */ShowInstalledExtensionsAction, ShowOutdatedExtensionsAction, ShowDisabledExtensionsAction,
ShowEnabledExtensionsAction, PredefinedExtensionFilterAction
ShowEnabledExtensionsAction, PredefinedExtensionFilterAction, RefreshExtensionsAction
} from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { OpenExtensionAuthoringDocsAction } from 'sql/workbench/contrib/extensions/browser/extensionsActions'; // {{ SQL CARBON EDIT }}
import { IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IWorkbenchExtensionEnablementService, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInFeatureExtensionsView, BuiltInThemesExtensionsView, BuiltInProgrammingLanguageExtensionsView, ServerExtensionsView, DefaultRecommendedExtensionsView, OutdatedExtensionsView, InstalledExtensionsView, SearchBuiltInExtensionsView } from 'vs/workbench/contrib/extensions/browser/extensionsViews';
import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInFeatureExtensionsView, BuiltInThemesExtensionsView, BuiltInProgrammingLanguageExtensionsView, ServerInstalledExtensionsView, DefaultRecommendedExtensionsView } from 'vs/workbench/contrib/extensions/browser/extensionsViews';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import Severity from 'vs/base/common/severity';
import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IViewsRegistry, IViewDescriptor, Extensions, ViewContainer, IViewDescriptorService, IAddedViewDescriptorRef } from 'vs/workbench/common/views';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { getMaliciousExtensionsSet } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
@@ -60,8 +60,12 @@ import { DragAndDropObserver } from 'vs/workbench/browser/dnd';
import { URI } from 'vs/base/common/uri';
import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { isWeb } from 'vs/base/common/platform';
import { memoize } from 'vs/base/common/decorators';
import { filterIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons';
const NonEmptyWorkspaceContext = new RawContextKey<boolean>('nonEmptyWorkspace', false);
const DefaultViewsContext = new RawContextKey<boolean>('defaultExtensionViews', true);
const SearchMarketplaceExtensionsContext = new RawContextKey<boolean>('searchMarketplaceExtensions', false);
const SearchIntalledExtensionsContext = new RawContextKey<boolean>('searchInstalledExtensions', false);
@@ -69,6 +73,7 @@ const SearchOutdatedExtensionsContext = new RawContextKey<boolean>('searchOutdat
const SearchEnabledExtensionsContext = new RawContextKey<boolean>('searchEnabledExtensions', false);
const SearchDisabledExtensionsContext = new RawContextKey<boolean>('searchDisabledExtensions', false);
const HasInstalledExtensionsContext = new RawContextKey<boolean>('hasInstalledExtensions', true);
const HasInstalledWebExtensionsContext = new RawContextKey<boolean>('hasInstalledWebExtensions', false);
const BuiltInExtensionsContext = new RawContextKey<boolean>('builtInExtensions', false);
const SearchBuiltInExtensionsContext = new RawContextKey<boolean>('searchBuiltInExtensions', false);
const RecommendedExtensionsContext = new RawContextKey<boolean>('recommendedExtensions', false);
@@ -80,7 +85,8 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio
constructor(
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
@ILabelService private readonly labelService: ILabelService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IContextKeyService private readonly contextKeyService: IContextKeyService
) {
this.container = viewDescriptorService.getViewContainerById(VIEWLET_ID)!;
this.registerViews();
@@ -135,27 +141,71 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio
servers.push(this.extensionManagementServerService.remoteExtensionManagementServer);
}
const getViewName = (viewTitle: string, server: IExtensionManagementServer): string => {
if (servers.length) {
const serverLabel = server === this.extensionManagementServerService.webExtensionManagementServer && !this.extensionManagementServerService.localExtensionManagementServer ? localize('local', "Local") : server.label;
return servers.length > 1 ? `${serverLabel} - ${viewTitle}` : viewTitle;
if (servers.length > 1) {
// In Web, use view title as is for remote server, when web extension server is enabled and no web extensions are installed
if (isWeb && server === this.extensionManagementServerService.remoteExtensionManagementServer &&
this.extensionManagementServerService.webExtensionManagementServer && !this.contextKeyService.getContextKeyValue<boolean>('hasInstalledWebExtensions')) {
return viewTitle;
}
return `${server.label} - ${viewTitle}`;
}
return viewTitle;
};
let installedWebExtensionsContextChangeEvent = Event.None;
if (this.extensionManagementServerService.webExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) {
const interestingContextKeys = new Set();
interestingContextKeys.add('hasInstalledWebExtensions');
installedWebExtensionsContextChangeEvent = Event.filter(this.contextKeyService.onDidChangeContext, e => e.affectsSome(interestingContextKeys));
}
const serverLabelChangeEvent = Event.any(this.labelService.onDidChangeFormatters, installedWebExtensionsContextChangeEvent);
for (const server of servers) {
const getInstalledViewName = (): string => getViewName(localize('installed', "Installed"), server);
const onDidChangeServerLabel: EventOf<void> = EventOf.map(this.labelService.onDidChangeFormatters, () => undefined);
const onDidChangeTitle = Event.map<void, string>(serverLabelChangeEvent, () => getInstalledViewName());
const id = servers.length > 1 ? `workbench.views.extensions.${server.id}.installed` : `workbench.views.extensions.installed`;
const isWebServer = server === this.extensionManagementServerService.webExtensionManagementServer;
if (!isWebServer) {
/* Empty installed extensions view */
viewDescriptors.push({
id: `${id}.empty`,
get name() { return getInstalledViewName(); },
weight: 100,
order: 1,
when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.not('hasInstalledExtensions')),
/* Empty installed extensions view shall have fixed height */
ctorDescriptor: new SyncDescriptor(ServerInstalledExtensionsView, [{ server, fixedHeight: true, onDidChangeTitle }]),
/* Empty installed extensions views shall not be allowed to hidden */
canToggleVisibility: false
});
}
/* Installed extensions view */
viewDescriptors.push({
id: servers.length > 1 ? `workbench.views.extensions.${server.id}.installed` : `workbench.views.extensions.installed`,
id,
get name() { return getInstalledViewName(); },
ctorDescriptor: new SyncDescriptor(ServerExtensionsView, [server, EventOf.map<void, string>(onDidChangeServerLabel, () => getInstalledViewName())]),
when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions')),
weight: 100,
order: 2,
/* Installed extensions views shall not be hidden when there are more than one server */
order: 1,
when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), isWebServer ? ContextKeyExpr.has('hasInstalledWebExtensions') : ContextKeyExpr.has('hasInstalledExtensions')),
ctorDescriptor: new SyncDescriptor(ServerInstalledExtensionsView, [{ server, onDidChangeTitle }]),
/* Installed extensions views shall not be allowed to hidden when there are more than one server */
canToggleVisibility: servers.length === 1
});
}
/*
* Default popular extensions view
* Separate view for popular extensions required as we need to show popular and recommended sections
* in the default view when there is no search text, and user has no installed extensions.
*/
// {{SQL CARBON EDIT}} -- remove "Popular" view
// viewDescriptors.push({
// id: 'workbench.views.extensions.popular',
// name: localize('popularExtensions', "Popular"),
// ctorDescriptor: new SyncDescriptor(ExtensionsListView, [{}]),
// when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.not('hasInstalledExtensions')),
// weight: 60,
// order: 2,
// canToggleVisibility: false
// });
/*
* Default recommended extensions view
* When user has installed extensions, this is shown along with the views for enabled & disabled extensions
@@ -163,8 +213,8 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio
*/
viewDescriptors.push({
id: 'extensions.recommendedList',
name: localize('recommendedExtensions', "Recommended"),
ctorDescriptor: new SyncDescriptor(DefaultRecommendedExtensionsView),
name: localize('recommendedExtensions', "Marketplace"), // {{SQL CARBON EDIT}} - change name to marketplace
ctorDescriptor: new SyncDescriptor(DefaultRecommendedExtensionsView, [{}]),
when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.not('config.extensions.showRecommendationsOnlyOnDemand')),
weight: 40,
order: 3,
@@ -180,7 +230,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio
viewDescriptors.push({
id: 'workbench.views.extensions.enabled',
name: localize('enabledExtensions', "Enabled"),
ctorDescriptor: new SyncDescriptor(EnabledExtensionsView),
ctorDescriptor: new SyncDescriptor(EnabledExtensionsView, [{}]),
when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions')),
hideByDefault: true,
weight: 40,
@@ -195,7 +245,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio
viewDescriptors.push({
id: 'workbench.views.extensions.disabled',
name: localize('disabledExtensions', "Disabled"),
ctorDescriptor: new SyncDescriptor(DisabledExtensionsView),
ctorDescriptor: new SyncDescriptor(DisabledExtensionsView, [{}]),
when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions')),
hideByDefault: true,
weight: 10,
@@ -217,7 +267,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio
viewDescriptors.push({
id: 'workbench.views.extensions.marketplace',
name: localize('marketPlace', "Marketplace"),
ctorDescriptor: new SyncDescriptor(ExtensionsListView),
ctorDescriptor: new SyncDescriptor(ExtensionsListView, [{}]),
when: ContextKeyExpr.and(ContextKeyExpr.has('searchMarketplaceExtensions')),
});
@@ -227,7 +277,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio
viewDescriptors.push({
id: 'workbench.views.extensions.searchInstalled',
name: localize('installed', "Installed"),
ctorDescriptor: new SyncDescriptor(InstalledExtensionsView),
ctorDescriptor: new SyncDescriptor(ExtensionsListView, [{}]),
when: ContextKeyExpr.and(ContextKeyExpr.has('searchInstalledExtensions')),
});
@@ -237,7 +287,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio
viewDescriptors.push({
id: 'workbench.views.extensions.searchEnabled',
name: localize('enabled', "Enabled"),
ctorDescriptor: new SyncDescriptor(EnabledExtensionsView),
ctorDescriptor: new SyncDescriptor(ExtensionsListView, [{}]),
when: ContextKeyExpr.and(ContextKeyExpr.has('searchEnabledExtensions')),
});
@@ -247,7 +297,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio
viewDescriptors.push({
id: 'workbench.views.extensions.searchDisabled',
name: localize('disabled', "Disabled"),
ctorDescriptor: new SyncDescriptor(DisabledExtensionsView),
ctorDescriptor: new SyncDescriptor(ExtensionsListView, [{}]),
when: ContextKeyExpr.and(ContextKeyExpr.has('searchDisabledExtensions')),
});
@@ -257,7 +307,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio
viewDescriptors.push({
id: 'workbench.views.extensions.searchOutdated',
name: localize('outdated', "Outdated"),
ctorDescriptor: new SyncDescriptor(OutdatedExtensionsView),
ctorDescriptor: new SyncDescriptor(ExtensionsListView, [{}]),
when: ContextKeyExpr.and(ContextKeyExpr.has('searchOutdatedExtensions')),
});
@@ -267,7 +317,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio
viewDescriptors.push({
id: 'workbench.views.extensions.searchBuiltin',
name: localize('builtin', "Builtin"),
ctorDescriptor: new SyncDescriptor(SearchBuiltInExtensionsView),
ctorDescriptor: new SyncDescriptor(ExtensionsListView, [{}]),
when: ContextKeyExpr.and(ContextKeyExpr.has('searchBuiltInExtensions')),
});
@@ -280,15 +330,15 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio
viewDescriptors.push({
id: 'workbench.views.extensions.workspaceRecommendations',
name: localize('workspaceRecommendedExtensions', "Workspace Recommendations"),
ctorDescriptor: new SyncDescriptor(WorkspaceRecommendedExtensionsView),
when: ContextKeyExpr.and(ContextKeyExpr.has('recommendedExtensions'), ContextKeyExpr.has('nonEmptyWorkspace')),
ctorDescriptor: new SyncDescriptor(WorkspaceRecommendedExtensionsView, [{}]),
when: ContextKeyExpr.and(ContextKeyExpr.has('recommendedExtensions'), WorkbenchStateContext.notEqualsTo('empty')),
order: 1
});
viewDescriptors.push({
id: 'workbench.views.extensions.otherRecommendations',
name: localize('otherRecommendedExtensions', "Other Recommendations"),
ctorDescriptor: new SyncDescriptor(RecommendedExtensionsView),
ctorDescriptor: new SyncDescriptor(RecommendedExtensionsView, [{}]),
when: ContextKeyExpr.has('recommendedExtensions'),
order: 2
});
@@ -302,21 +352,21 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio
viewDescriptors.push({
id: 'workbench.views.extensions.builtinFeatureExtensions',
name: localize('builtinFeatureExtensions', "Features"),
ctorDescriptor: new SyncDescriptor(BuiltInFeatureExtensionsView),
ctorDescriptor: new SyncDescriptor(BuiltInFeatureExtensionsView, [{}]),
when: ContextKeyExpr.has('builtInExtensions'),
});
viewDescriptors.push({
id: 'workbench.views.extensions.builtinThemeExtensions',
name: localize('builtInThemesExtensions', "Themes"),
ctorDescriptor: new SyncDescriptor(BuiltInThemesExtensionsView),
ctorDescriptor: new SyncDescriptor(BuiltInThemesExtensionsView, [{}]),
when: ContextKeyExpr.has('builtInExtensions'),
});
viewDescriptors.push({
id: 'workbench.views.extensions.builtinProgrammingLanguageExtensions',
name: localize('builtinProgrammingLanguageExtensions', "Programming Languages"),
ctorDescriptor: new SyncDescriptor(BuiltInProgrammingLanguageExtensionsView),
ctorDescriptor: new SyncDescriptor(BuiltInProgrammingLanguageExtensionsView, [{}]),
when: ContextKeyExpr.has('builtInExtensions'),
});
@@ -328,8 +378,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio
export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IExtensionsViewPaneContainer {
private readonly _onSearchChange: Emitter<string> = this._register(new Emitter<string>());
private readonly onSearchChange: EventOf<string> = this._onSearchChange.event;
private nonEmptyWorkspaceContextKey: IContextKey<boolean>;
private readonly onSearchChange: Event<string> = this._onSearchChange.event;
private defaultViewsContextKey: IContextKey<boolean>;
private searchMarketplaceExtensionsContextKey: IContextKey<boolean>;
private searchInstalledExtensionsContextKey: IContextKey<boolean>;
@@ -337,6 +386,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
private searchEnabledExtensionsContextKey: IContextKey<boolean>;
private searchDisabledExtensionsContextKey: IContextKey<boolean>;
private hasInstalledExtensionsContextKey: IContextKey<boolean>;
private hasInstalledWebExtensionsContextKey: IContextKey<boolean>;
private builtInExtensionsContextKey: IContextKey<boolean>;
private searchBuiltInExtensionsContextKey: IContextKey<boolean>;
private recommendedExtensionsContextKey: IContextKey<boolean>;
@@ -346,6 +396,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
private searchBox: SuggestEnabledInput | undefined;
private readonly searchViewletState: MementoObject;
private readonly sortActions: ChangeSortAction[];
private secondaryActions: IAction[] | undefined = undefined;
constructor(
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@@ -353,7 +404,8 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
@IProgressService private readonly progressService: IProgressService,
@IInstantiationService instantiationService: IInstantiationService,
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@INotificationService private readonly notificationService: INotificationService,
@IViewletService private readonly viewletService: IViewletService,
@@ -366,11 +418,11 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
@IExtensionService extensionService: IExtensionService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IPreferencesService private readonly preferencesService: IPreferencesService,
@ICommandService private readonly commandService: ICommandService
) {
super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService);
this.searchDelayer = new Delayer(500);
this.nonEmptyWorkspaceContextKey = NonEmptyWorkspaceContext.bindTo(contextKeyService);
this.defaultViewsContextKey = DefaultViewsContext.bindTo(contextKeyService);
this.searchMarketplaceExtensionsContextKey = SearchMarketplaceExtensionsContext.bindTo(contextKeyService);
this.searchInstalledExtensionsContextKey = SearchIntalledExtensionsContext.bindTo(contextKeyService);
@@ -378,22 +430,30 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
this.searchEnabledExtensionsContextKey = SearchEnabledExtensionsContext.bindTo(contextKeyService);
this.searchDisabledExtensionsContextKey = SearchDisabledExtensionsContext.bindTo(contextKeyService);
this.hasInstalledExtensionsContextKey = HasInstalledExtensionsContext.bindTo(contextKeyService);
this.hasInstalledWebExtensionsContextKey = HasInstalledWebExtensionsContext.bindTo(contextKeyService);
this.builtInExtensionsContextKey = BuiltInExtensionsContext.bindTo(contextKeyService);
this.searchBuiltInExtensionsContextKey = SearchBuiltInExtensionsContext.bindTo(contextKeyService);
this.recommendedExtensionsContextKey = RecommendedExtensionsContext.bindTo(contextKeyService);
this._register(this.viewletService.onDidViewletOpen(this.onViewletOpen, this));
this.searchViewletState = this.getMemento(StorageScope.WORKSPACE);
this.extensionManagementService.getInstalled(ExtensionType.User).then(result => {
this.hasInstalledExtensionsContextKey.set(result.length > 0);
});
this.searchViewletState = this.getMemento(StorageScope.WORKSPACE, StorageTarget.USER);
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(AutoUpdateConfigurationKey)) {
this.secondaryActions = undefined;
this.updateTitleArea();
}
}, this));
if (extensionManagementServerService.webExtensionManagementServer) {
this._register(extensionsWorkbenchService.onChange(() => {
// show installed web extensions view only when it is not visible
// Do not hide the view automatically when it is visible
if (!this.hasInstalledWebExtensionsContextKey.get()) {
this.updateInstalledWebExtensionsContext();
}
}));
}
this.sortActions = [
// this._register(this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.install', localize('sort by installs', "Install Count"), this.onSearchChange, 'installs')), // {{SQL CARBON EDIT}}
// this._register(this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.rating', localize('sort by rating', "Rating"), this.onSearchChange, 'rating')), // {{SQL CARBON EDIT}}
@@ -426,6 +486,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
provideResults: (query: string) => Query.suggestions(query)
}, placeholder, 'extensions:searchinput', { placeholderText: placeholder, value: searchValue }));
this.updateInstalledExtensionsContexts();
if (this.searchBox.getValue()) {
this.triggerSearch();
}
@@ -478,7 +539,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
try {
// Attempt to install the extension(s)
await this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL).run(vsixPaths);
await this.commandService.executeCommand(INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, vsixPaths);
}
catch (err) {
this.notificationService.error(err);
@@ -502,7 +563,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
this.root.classList.toggle('narrow', dimension.width <= 300);
}
if (this.searchBox) {
this.searchBox.layout({ height: 20, width: dimension.width - 34 });
this.searchBox.layout(new Dimension(dimension.width - 34, 20));
}
super.layout(new Dimension(dimension.width, dimension.height - 41));
}
@@ -511,20 +572,19 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
return 400;
}
@memoize
getActions(): IAction[] {
const filterActions: IAction[] = [];
// Local extensions filters
filterActions.push(...[
this.instantiationService.createInstance(ShowBuiltInExtensionsAction, ShowBuiltInExtensionsAction.ID, localize('builtin filter', "Built-in")),
this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, localize('installed filter', "Installed")),
this.instantiationService.createInstance(ShowEnabledExtensionsAction, ShowEnabledExtensionsAction.ID, localize('enabled filter', "Enabled")),
this.instantiationService.createInstance(ShowDisabledExtensionsAction, ShowDisabledExtensionsAction.ID, localize('disabled filter', "Disabled")),
this.instantiationService.createInstance(ShowOutdatedExtensionsAction, ShowOutdatedExtensionsAction.ID, localize('outdated filter', "Outdated")),
]);
let filterActions: IAction[] = [
this._register(this.instantiationService.createInstance(ShowBuiltInExtensionsAction, ShowBuiltInExtensionsAction.ID, localize('builtin filter', "Built-in"))),
this._register(this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, localize('installed filter', "Installed"))),
this._register(this.instantiationService.createInstance(ShowEnabledExtensionsAction, ShowEnabledExtensionsAction.ID, localize('enabled filter', "Enabled"))),
this._register(this.instantiationService.createInstance(ShowDisabledExtensionsAction, ShowDisabledExtensionsAction.ID, localize('disabled filter', "Disabled"))),
this._register(this.instantiationService.createInstance(ShowOutdatedExtensionsAction, ShowOutdatedExtensionsAction.ID, localize('outdated filter', "Outdated"))),
];
if (this.extensionGalleryService.isEnabled()) {
const galleryFilterActions = [
filterActions = [
// this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.featured', localize('featured filter', "Featured"), '@featured'), // {{SQL CARBON EDIT}}
// this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.popular', localize('most popular filter', "Most Popular"), '@popular'), // {{SQL CARBON EDIT}}
this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.recommended', localize('most popular recommended', "Recommended"), '@recommended'),
@@ -532,51 +592,62 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
new Separator(),
new SubmenuAction('workbench.extensions.action.filterExtensionsByCategory', localize('filter by category', "Category"), EXTENSION_CATEGORIES.map(category => this.instantiationService.createInstance(SearchCategoryAction, `extensions.actions.searchByCategory.${category}`, category, category))),
new Separator(),
...filterActions,
this._register(new Separator()),
this._register(new SubmenuAction('workbench.extensions.action.sortBy', localize('sorty by', "Sort By"), this.sortActions)),
];
filterActions.splice(0, 0, ...galleryFilterActions);
filterActions.push(...[
new Separator(),
new SubmenuAction('workbench.extensions.action.sortBy', localize('sorty by', "Sort By"), this.sortActions),
]);
}
return [
new SubmenuAction('workbench.extensions.action.filterExtensions', localize('filterExtensions', "Filter Extensions..."), filterActions, 'codicon-filter'),
this.instantiationService.createInstance(ClearExtensionsInputAction, ClearExtensionsInputAction.ID, ClearExtensionsInputAction.LABEL, this.onSearchChange, this.searchBox ? this.searchBox.getValue() : ''),
this._register(new SubmenuAction('workbench.extensions.action.filterExtensions', localize('filterExtensions', "Filter Extensions..."), filterActions, ThemeIcon.asClassName(filterIcon))),
this._register(this.instantiationService.createInstance(RefreshExtensionsAction, RefreshExtensionsAction.ID, RefreshExtensionsAction.LABEL)),
this._register(this.instantiationService.createInstance(ClearExtensionsInputAction, ClearExtensionsInputAction.ID, ClearExtensionsInputAction.LABEL, this.onSearchChange, () => this.searchBox!.getValue() || '')),
];
}
getSecondaryActions(): IAction[] {
const actions: IAction[] = [];
if (!this.secondaryActions) {
this.secondaryActions = [];
this.secondaryActions.push(this.instantiationService.createInstance(CheckForUpdatesAction, CheckForUpdatesAction.ID, CheckForUpdatesAction.LABEL));
if (this.configurationService.getValue(AutoUpdateConfigurationKey)) {
this.secondaryActions.push(this.instantiationService.createInstance(DisableAutoUpdateAction, DisableAutoUpdateAction.ID, DisableAutoUpdateAction.LABEL));
} else {
this.secondaryActions.push(this.instantiationService.createInstance(UpdateAllAction, UpdateAllAction.ID, UpdateAllAction.LABEL, false), this.instantiationService.createInstance(EnableAutoUpdateAction, EnableAutoUpdateAction.ID, EnableAutoUpdateAction.LABEL));
}
this.secondaryActions.push(new Separator());
this.secondaryActions.push(this.instantiationService.createInstance(EnableAllAction, EnableAllAction.ID, EnableAllAction.LABEL, false));
this.secondaryActions.push(this.instantiationService.createInstance(DisableAllAction, DisableAllAction.ID, DisableAllAction.LABEL, false));
this.secondaryActions.push(new Separator());
this.secondaryActions.push(this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL));
this.secondaryActions.push(this.instantiationService.createInstance(OpenExtensionAuthoringDocsAction, OpenExtensionAuthoringDocsAction.ID, OpenExtensionAuthoringDocsAction.LABEL)); // {{SQL CARBON EDIT}}
actions.push(this.instantiationService.createInstance(CheckForUpdatesAction, CheckForUpdatesAction.ID, CheckForUpdatesAction.LABEL));
if (this.configurationService.getValue(AutoUpdateConfigurationKey)) {
actions.push(this.instantiationService.createInstance(DisableAutoUpdateAction, DisableAutoUpdateAction.ID, DisableAutoUpdateAction.LABEL));
} else {
actions.push(this.instantiationService.createInstance(UpdateAllAction, UpdateAllAction.ID, UpdateAllAction.LABEL), this.instantiationService.createInstance(EnableAutoUpdateAction, EnableAutoUpdateAction.ID, EnableAutoUpdateAction.LABEL));
}
actions.push(new Separator());
actions.push(this.instantiationService.createInstance(EnableAllAction, EnableAllAction.ID, EnableAllAction.LABEL));
actions.push(this.instantiationService.createInstance(DisableAllAction, DisableAllAction.ID, DisableAllAction.LABEL));
actions.push(new Separator());
actions.push(this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL));
actions.push(this.instantiationService.createInstance(OpenExtensionAuthoringDocsAction, OpenExtensionAuthoringDocsAction.ID, OpenExtensionAuthoringDocsAction.LABEL)); // {{SQL CARBON EDIT}}
return actions;
return this.secondaryActions;
}
search(value: string, refresh: boolean = false): void {
if (this.searchBox) {
if (this.searchBox.getValue() !== value) {
this.searchBox.setValue(value);
} else if (refresh) {
this.doSearch();
}
search(value: string): void {
if (this.searchBox && this.searchBox.getValue() !== value) {
this.searchBox.setValue(value);
}
}
async refresh(): Promise<void> {
await this.updateInstalledExtensionsContexts();
this.doSearch(true);
}
private async updateInstalledExtensionsContexts(): Promise<void> {
const result = await this.extensionsWorkbenchService.queryLocal();
this.hasInstalledExtensionsContextKey.set(result.some(r => !r.isBuiltin));
this.updateInstalledWebExtensionsContext();
}
private updateInstalledWebExtensionsContext(): void {
this.hasInstalledWebExtensionsContextKey.set(!!this.extensionManagementServerService.webExtensionManagementServer && this.extensionsWorkbenchService.installed.some(r => r.server === this.extensionManagementServerService.webExtensionManagementServer));
}
private triggerSearch(): void {
this.searchDelayer.trigger(() => this.doSearch(), this.searchBox && this.searchBox.getValue() ? 500 : 0).then(undefined, err => this.onError(err));
}
@@ -603,7 +674,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
super.saveState();
}
private doSearch(): Promise<void> {
private doSearch(refresh?: boolean): Promise<void> {
const value = this.normalizedQuery();
const isRecommendedExtensionsQuery = ExtensionsListView.isRecommendedExtensionsQuery(value);
this.searchInstalledExtensionsContextKey.set(ExtensionsListView.isInstalledExtensionsQuery(value));
@@ -614,11 +685,11 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
this.builtInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value));
this.recommendedExtensionsContextKey.set(isRecommendedExtensionsQuery);
this.searchMarketplaceExtensionsContextKey.set(!!value && !ExtensionsListView.isLocalExtensionsQuery(value) && !isRecommendedExtensionsQuery);
this.nonEmptyWorkspaceContextKey.set(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY);
this.defaultViewsContextKey.set(!value);
this.updateInstalledWebExtensionsContext();
return this.progress(Promise.all(this.panes.map(view =>
(<ExtensionsListView>view).show(this.normalizedQuery())
(<ExtensionsListView>view).show(this.normalizedQuery(), refresh)
.then(model => this.alertSearchResult(model.length, view.id))
))).then(() => undefined);
}
@@ -768,7 +839,7 @@ export class MaliciousExtensionChecker implements IWorkbenchContribution {
.filter(e => maliciousSet.has(e.identifier.id));
if (maliciousExtensions.length) {
return Promise.all(maliciousExtensions.map(e => this.extensionsManagementService.uninstall(e, true).then(() => {
return Promise.all(maliciousExtensions.map(e => this.extensionsManagementService.uninstall(e).then(() => {
this.notificationService.prompt(
Severity.Warning,
localize('malicious warning', "We have uninstalled '{0}' which was reported to be problematic.", e.identifier.id),

View File

@@ -4,22 +4,23 @@
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { Disposable } from 'vs/base/common/lifecycle';
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { Event, Emitter } from 'vs/base/common/event';
import { isPromiseCanceledError, getErrorMessage } from 'vs/base/common/errors';
import { PagedModel, IPagedModel, IPager, DelayedPagedModel } from 'vs/base/common/paging';
import { SortBy, SortOrder, IQueryOptions, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionManagementServer, IExtensionManagementServerService, IExtensionRecommendationsService, IExtensionRecommendation, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { SortBy, SortOrder, IQueryOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionManagementServer, IExtensionManagementServerService, EnablementState, IWorkbenchExtensioManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { append, $ } from 'vs/base/browser/dom';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Delegate, Renderer, IExtensionsViewState } from 'vs/workbench/contrib/extensions/browser/extensionsList';
import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { Delegate, Renderer, IExtensionsViewState, EXTENSION_LIST_ELEMENT_HEIGHT } from 'vs/workbench/contrib/extensions/browser/extensionsList';
import { ExtensionState, IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery';
import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { attachBadgeStyler } from 'vs/platform/theme/common/styler';
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@@ -37,17 +38,18 @@ import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list';
import { createErrorWithActions } from 'vs/base/common/errorsWithActions';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IAction, Action, Separator } from 'vs/base/common/actions';
import { ExtensionType, ExtensionIdentifier, IExtensionDescription, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions';
import { ExtensionIdentifier, ExtensionType, IExtensionDescription, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; // {{ SQL CARBON EDIT }}
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { IProductService } from 'vs/platform/product/common/productService';
import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { IMenuService } from 'vs/platform/actions/common/actions';
import { IViewDescriptorService } from 'vs/workbench/common/views';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { configureRecommendedIcon, installLocalInRemoteIcon, installWorkspaceRecommendedIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons';
// Extensions that are automatically classified as Programming Language extensions, but should be Feature extensions
const FORCE_FEATURE_EXTENSIONS = ['vscode.git', 'vscode.search-result'];
@@ -73,15 +75,22 @@ class ExtensionsViewState extends Disposable implements IExtensionsViewState {
}
}
export interface ExtensionsListViewOptions extends IViewletViewOptions {
export interface ExtensionsListViewOptions {
server?: IExtensionManagementServer;
fixedHeight?: boolean;
onDidChangeTitle?: Event<string>;
}
class ExtensionListViewWarning extends Error { }
interface IQueryResult {
readonly model: IPagedModel<IExtension>;
readonly onDidChangeModel?: Event<IPagedModel<IExtension>>;
readonly disposables: DisposableStore;
}
export class ExtensionsListView extends ViewPane {
protected readonly server: IExtensionManagementServer | undefined;
private bodyTemplate: {
messageContainer: HTMLElement;
messageSeverityIcon: HTMLElement;
@@ -91,9 +100,11 @@ export class ExtensionsListView extends ViewPane {
private badge: CountBadge | undefined;
private list: WorkbenchPagedList<IExtension> | null = null;
private queryRequest: { query: string, request: CancelablePromise<IPagedModel<IExtension>> } | null = null;
private queryResult: IQueryResult | undefined;
constructor(
options: ExtensionsListViewOptions,
protected readonly options: ExtensionsListViewOptions,
viewletViewOptions: IViewletViewOptions,
@INotificationService protected notificationService: INotificationService,
@IKeybindingService keybindingService: IKeybindingService,
@IContextMenuService contextMenuService: IContextMenuService,
@@ -107,16 +118,22 @@ export class ExtensionsListView extends ViewPane {
@IWorkspaceContextService protected contextService: IWorkspaceContextService,
@IExperimentService private readonly experimentService: IExperimentService,
@IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService,
@IExtensionManagementService protected readonly extensionManagementService: IExtensionManagementService,
@IWorkbenchExtensioManagementService protected readonly extensionManagementService: IWorkbenchExtensioManagementService,
@IProductService protected readonly productService: IProductService,
@IContextKeyService contextKeyService: IContextKeyService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IMenuService private readonly menuService: IMenuService,
@IOpenerService openerService: IOpenerService,
@IPreferencesService private readonly preferencesService: IPreferencesService,
@IStorageService private readonly storageService: IStorageService,
) {
super({ ...(options as IViewPaneOptions), showActionsAlways: true }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
this.server = options.server;
super({
...(viewletViewOptions as IViewPaneOptions),
showActionsAlways: true,
maximumBodySize: options.fixedHeight ? storageService.getNumber(viewletViewOptions.id, StorageScope.GLOBAL, 0) : undefined
}, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
if (this.options.onDidChangeTitle) {
this._register(this.options.onDidChangeTitle(title => this.updateTitle(title)));
}
}
protected renderHeader(container: HTMLElement): void {
@@ -181,15 +198,20 @@ export class ExtensionsListView extends ViewPane {
}
}
async show(query: string): Promise<IPagedModel<IExtension>> {
async show(query: string, refresh?: boolean): Promise<IPagedModel<IExtension>> {
if (this.queryRequest) {
if (this.queryRequest.query === query) {
if (!refresh && this.queryRequest.query === query) {
return this.queryRequest.request;
}
this.queryRequest.request.cancel();
this.queryRequest = null;
}
if (this.queryResult) {
this.queryResult.disposables.dispose();
this.queryResult = undefined;
}
const parsedQuery = Query.parse(query);
let options: IQueryOptions = {
@@ -203,23 +225,25 @@ export class ExtensionsListView extends ViewPane {
case 'publishedDate': options.sortBy = SortBy.PublishedDate; break;
}
const successCallback = (model: IPagedModel<IExtension>) => {
this.queryRequest = null;
this.setModel(model);
return model;
};
const errorCallback = (e: any) => {
const model = new PagedModel([]);
if (!isPromiseCanceledError(e)) {
this.queryRequest = null;
this.setModel(model, e);
const request = createCancelablePromise(async token => {
try {
this.queryResult = await this.query(parsedQuery, options, token);
const model = this.queryResult.model;
this.setModel(model);
if (this.queryResult.onDidChangeModel) {
this.queryResult.disposables.add(this.queryResult.onDidChangeModel(model => this.updateModel(model)));
}
return model;
} catch (e) {
const model = new PagedModel([]);
if (!isPromiseCanceledError(e)) {
this.setModel(model, e);
}
return this.list ? this.list.model : model;
}
return this.list ? this.list.model : model;
};
});
const request = createCancelablePromise(token => this.query(parsedQuery, options, token).then(successCallback).catch(errorCallback));
request.finally(() => this.queryRequest = null);
this.queryRequest = { query, request };
return request;
}
@@ -250,7 +274,7 @@ export class ExtensionsListView extends ViewPane {
getActions: () => actions.slice(0, actions.length - 1)
});
} else if (e.element) {
const groups = getContextMenuActions(this.menuService, this.contextKeyService.createScoped(), this.instantiationService, e.element);
const groups = getContextMenuActions(e.element, false, this.instantiationService);
groups.forEach(group => group.forEach(extensionAction => {
if (extensionAction instanceof ExtensionAction) {
extensionAction.extension = e.element!;
@@ -268,7 +292,7 @@ export class ExtensionsListView extends ViewPane {
}
}
private async query(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
private async query(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IQueryResult> {
const idRegex = /@id:(([a-z0-9A-Z][a-z0-9\-A-Z]*)\.([a-z0-9A-Z][a-z0-9\-A-Z]*))/g;
const ids: string[] = [];
let idMatch;
@@ -277,21 +301,26 @@ export class ExtensionsListView extends ViewPane {
ids.push(name);
}
if (ids.length) {
return this.queryByIds(ids, options, token);
const model = await this.queryByIds(ids, options, token);
return { model, disposables: new DisposableStore() };
}
if (ExtensionsListView.isLocalExtensionsQuery(query.value) || /@builtin/.test(query.value)) {
if (ExtensionsListView.isLocalExtensionsQuery(query.value)) {
return this.queryLocal(query, options);
}
return this.queryGallery(query, options, token)
.then(null, e => {
console.warn('Error querying extensions gallery', getErrorMessage(e));
return Promise.reject(new ExtensionListViewWarning(localize('galleryError', "We cannot connect to the Extensions Marketplace at this time, please try again later.")));
});
try {
const model = await this.queryGallery(query, options, token);
return { model, disposables: new DisposableStore() };
} catch (e) {
console.warn('Error querying extensions gallery', getErrorMessage(e));
return Promise.reject(new ExtensionListViewWarning(localize('galleryError', "We cannot connect to the Extensions Marketplace at this time, please try again later.")));
}
}
private async queryByIds(ids: string[], options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const idsSet: Set<string> = ids.reduce((result, id) => { result.add(id.toLowerCase()); return result; }, new Set<string>());
const result = (await this.extensionsWorkbenchService.queryLocal(this.server))
const result = (await this.extensionsWorkbenchService.queryLocal(this.options.server))
.filter(e => idsSet.has(e.identifier.id.toLowerCase()));
if (result.length) {
@@ -302,58 +331,120 @@ export class ExtensionsListView extends ViewPane {
.then(pager => this.getPagedModel(pager));
}
private async queryLocal(query: Query, options: IQueryOptions): Promise<IPagedModel<IExtension>> {
let value = query.value;
if (/@builtin/i.test(value)) {
const showThemesOnly = /@builtin:themes/i.test(value);
if (showThemesOnly) {
value = value.replace(/@builtin:themes/g, '');
}
const showBasicsOnly = /@builtin:basics/i.test(value);
if (showBasicsOnly) {
value = value.replace(/@builtin:basics/g, '');
}
const showFeaturesOnly = /@builtin:features/i.test(value);
if (showFeaturesOnly) {
value = value.replace(/@builtin:features/g, '');
}
private async queryLocal(query: Query, options: IQueryOptions): Promise<IQueryResult> {
const local = await this.extensionsWorkbenchService.queryLocal(this.options.server);
const runningExtensions = await this.extensionService.getExtensions();
let { extensions, canIncludeInstalledExtensions } = this.filterLocal(local, runningExtensions, query, options);
const disposables = new DisposableStore();
const onDidChangeModel = disposables.add(new Emitter<IPagedModel<IExtension>>());
value = value.replace(/@builtin/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();
let result = await this.extensionsWorkbenchService.queryLocal(this.server);
result = result
.filter(e => e.type === ExtensionType.System && (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1));
const isThemeExtension = (e: IExtension): boolean => {
return (Array.isArray(e.local?.manifest?.contributes?.themes) && e.local!.manifest!.contributes!.themes.length > 0)
|| (Array.isArray(e.local?.manifest?.contributes?.iconThemes) && e.local!.manifest!.contributes!.iconThemes.length > 0);
};
if (showThemesOnly) {
const themesExtensions = result.filter(isThemeExtension);
return this.getPagedModel(this.sortExtensions(themesExtensions, options));
}
const isLangaugeBasicExtension = (e: IExtension): boolean => {
return FORCE_FEATURE_EXTENSIONS.indexOf(e.identifier.id) === -1
&& (Array.isArray(e.local?.manifest?.contributes?.grammars) && e.local!.manifest!.contributes!.grammars.length > 0);
};
if (showBasicsOnly) {
const basics = result.filter(isLangaugeBasicExtension);
return this.getPagedModel(this.sortExtensions(basics, options));
}
if (showFeaturesOnly) {
const others = result.filter(e => {
return e.local
&& e.local.manifest
&& !isThemeExtension(e)
&& !isLangaugeBasicExtension(e);
});
return this.getPagedModel(this.sortExtensions(others, options));
}
return this.getPagedModel(this.sortExtensions(result, options));
if (canIncludeInstalledExtensions) {
let isDisposed: boolean = false;
disposables.add(toDisposable(() => isDisposed = true));
disposables.add(Event.debounce(Event.any(
Event.filter(this.extensionsWorkbenchService.onChange, e => e?.state === ExtensionState.Installed),
this.extensionService.onDidChangeExtensions
), () => undefined)(async () => {
const local = this.options.server ? this.extensionsWorkbenchService.installed.filter(e => e.server === this.options.server) : this.extensionsWorkbenchService.local;
const runningExtensions = await this.extensionService.getExtensions();
const { extensions: newExtensions } = this.filterLocal(local, runningExtensions, query, options);
if (!isDisposed) {
const mergedExtensions = this.mergeAddedExtensions(extensions, newExtensions);
if (mergedExtensions) {
extensions = mergedExtensions;
onDidChangeModel.fire(new PagedModel(extensions));
}
}
}));
}
return {
model: new PagedModel(extensions),
onDidChangeModel: onDidChangeModel.event,
disposables
};
}
private filterLocal(local: IExtension[], runningExtensions: IExtensionDescription[], query: Query, options: IQueryOptions): { extensions: IExtension[], canIncludeInstalledExtensions: boolean } {
let value = query.value;
let extensions: IExtension[] = [];
let canIncludeInstalledExtensions = true;
if (/@builtin/i.test(value)) {
extensions = this.filterBuiltinExtensions(local, query, options);
canIncludeInstalledExtensions = false;
}
else if (/@installed/i.test(value)) {
extensions = this.filterInstalledExtensions(local, runningExtensions, query, options);
}
else if (/@outdated/i.test(value)) {
extensions = this.filterOutdatedExtensions(local, query, options);
}
else if (/@disabled/i.test(value)) {
extensions = this.filterDisabledExtensions(local, runningExtensions, query, options);
}
else if (/@enabled/i.test(value)) {
extensions = this.filterEnabledExtensions(local, runningExtensions, query, options);
}
return { extensions, canIncludeInstalledExtensions };
}
private filterBuiltinExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] {
let value = query.value;
const showThemesOnly = /@builtin:themes/i.test(value);
if (showThemesOnly) {
value = value.replace(/@builtin:themes/g, '');
}
const showBasicsOnly = /@builtin:basics/i.test(value);
if (showBasicsOnly) {
value = value.replace(/@builtin:basics/g, '');
}
const showFeaturesOnly = /@builtin:features/i.test(value);
if (showFeaturesOnly) {
value = value.replace(/@builtin:features/g, '');
}
value = value.replace(/@builtin/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();
let result = local
.filter(e => e.isBuiltin && (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1));
const isThemeExtension = (e: IExtension): boolean => {
return (Array.isArray(e.local?.manifest?.contributes?.themes) && e.local!.manifest!.contributes!.themes.length > 0)
|| (Array.isArray(e.local?.manifest?.contributes?.iconThemes) && e.local!.manifest!.contributes!.iconThemes.length > 0);
};
if (showThemesOnly) {
const themesExtensions = result.filter(isThemeExtension);
return this.sortExtensions(themesExtensions, options);
}
const isLangaugeBasicExtension = (e: IExtension): boolean => {
return FORCE_FEATURE_EXTENSIONS.indexOf(e.identifier.id) === -1
&& (Array.isArray(e.local?.manifest?.contributes?.grammars) && e.local!.manifest!.contributes!.grammars.length > 0);
};
if (showBasicsOnly) {
const basics = result.filter(isLangaugeBasicExtension);
return this.sortExtensions(basics, options);
}
if (showFeaturesOnly) {
const others = result.filter(e => {
return e.local
&& e.local.manifest
&& !isThemeExtension(e)
&& !isLangaugeBasicExtension(e);
});
return this.sortExtensions(others, options);
}
return this.sortExtensions(result, options);
}
private parseCategories(value: string): { value: string, categories: string[] } {
const categories: string[] = [];
value = value.replace(/\bcategory:("([^"]*)"|([^"]\S*))(\s+|\b|$)/g, (_, quotedCategory, category) => {
const entry = (category || quotedCategory || '').toLowerCase();
@@ -362,96 +453,118 @@ export class ExtensionsListView extends ViewPane {
}
return '';
});
return { value, categories };
}
if (/@installed/i.test(value)) {
// Show installed extensions
value = value.replace(/@installed/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();
private filterInstalledExtensions(local: IExtension[], runningExtensions: IExtensionDescription[], query: Query, options: IQueryOptions): IExtension[] {
let { value, categories } = this.parseCategories(query.value);
let result = await this.extensionsWorkbenchService.queryLocal(this.server);
value = value.replace(/@installed/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();
result = result
.filter(e => e.type === ExtensionType.User
&& (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)
&& (!categories.length || categories.some(category => (e.local && e.local.manifest.categories || []).some(c => c.toLowerCase() === category))));
let result = local
.filter(e => !e.isBuiltin
&& (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)
&& (!categories.length || categories.some(category => (e.local && e.local.manifest.categories || []).some(c => c.toLowerCase() === category))));
if (options.sortBy !== undefined) {
result = this.sortExtensions(result, options);
} else {
const runningExtensions = await this.extensionService.getExtensions();
const runningExtensionsById = runningExtensions.reduce((result, e) => { result.set(ExtensionIdentifier.toKey(e.identifier.value), e); return result; }, new Map<string, IExtensionDescription>());
result = result.sort((e1, e2) => {
const running1 = runningExtensionsById.get(ExtensionIdentifier.toKey(e1.identifier.id));
const isE1Running = running1 && this.extensionManagementServerService.getExtensionManagementServer(toExtension(running1)) === e1.server;
const running2 = runningExtensionsById.get(ExtensionIdentifier.toKey(e2.identifier.id));
const isE2Running = running2 && this.extensionManagementServerService.getExtensionManagementServer(toExtension(running2)) === e2.server;
if ((isE1Running && isE2Running)) {
return e1.displayName.localeCompare(e2.displayName);
if (options.sortBy !== undefined) {
result = this.sortExtensions(result, options);
} else {
const runningExtensionsById = runningExtensions.reduce((result, e) => { result.set(ExtensionIdentifier.toKey(e.identifier.value), e); return result; }, new Map<string, IExtensionDescription>());
result = result.sort((e1, e2) => {
const running1 = runningExtensionsById.get(ExtensionIdentifier.toKey(e1.identifier.id));
const isE1Running = running1 && this.extensionManagementServerService.getExtensionManagementServer(toExtension(running1)) === e1.server;
const running2 = runningExtensionsById.get(ExtensionIdentifier.toKey(e2.identifier.id));
const isE2Running = running2 && this.extensionManagementServerService.getExtensionManagementServer(toExtension(running2)) === e2.server;
if ((isE1Running && isE2Running)) {
return e1.displayName.localeCompare(e2.displayName);
}
const isE1LanguagePackExtension = e1.local && isLanguagePackExtension(e1.local.manifest);
const isE2LanguagePackExtension = e2.local && isLanguagePackExtension(e2.local.manifest);
if (!isE1Running && !isE2Running) {
if (isE1LanguagePackExtension) {
return -1;
}
const isE1LanguagePackExtension = e1.local && isLanguagePackExtension(e1.local.manifest);
const isE2LanguagePackExtension = e2.local && isLanguagePackExtension(e2.local.manifest);
if (!isE1Running && !isE2Running) {
if (isE1LanguagePackExtension) {
return -1;
}
if (isE2LanguagePackExtension) {
return 1;
}
return e1.displayName.localeCompare(e2.displayName);
if (isE2LanguagePackExtension) {
return 1;
}
if ((isE1Running && isE2LanguagePackExtension) || (isE2Running && isE1LanguagePackExtension)) {
return e1.displayName.localeCompare(e2.displayName);
}
return isE1Running ? -1 : 1;
});
return e1.displayName.localeCompare(e2.displayName);
}
if ((isE1Running && isE2LanguagePackExtension) || (isE2Running && isE1LanguagePackExtension)) {
return e1.displayName.localeCompare(e2.displayName);
}
return isE1Running ? -1 : 1;
});
}
return result;
}
private filterOutdatedExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] {
let { value, categories } = this.parseCategories(query.value);
value = value.replace(/@outdated/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();
const result = local
.sort((e1, e2) => e1.displayName.localeCompare(e2.displayName))
.filter(extension => extension.outdated
&& (extension.name.toLowerCase().indexOf(value) > -1 || extension.displayName.toLowerCase().indexOf(value) > -1)
&& (!categories.length || categories.some(category => !!extension.local && extension.local.manifest.categories!.some(c => c.toLowerCase() === category))));
return this.sortExtensions(result, options);
}
private filterDisabledExtensions(local: IExtension[], runningExtensions: IExtensionDescription[], query: Query, options: IQueryOptions): IExtension[] {
let { value, categories } = this.parseCategories(query.value);
value = value.replace(/@disabled/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();
const result = local
.sort((e1, e2) => e1.displayName.localeCompare(e2.displayName))
.filter(e => runningExtensions.every(r => !areSameExtensions({ id: r.identifier.value, uuid: r.uuid }, e.identifier))
&& (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)
&& (!categories.length || categories.some(category => (e.local && e.local.manifest.categories || []).some(c => c.toLowerCase() === category))));
return this.sortExtensions(result, options);
}
private filterEnabledExtensions(local: IExtension[], runningExtensions: IExtensionDescription[], query: Query, options: IQueryOptions): IExtension[] {
let { value, categories } = this.parseCategories(query.value);
value = value ? value.replace(/@enabled/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase() : '';
local = local.filter(e => !e.isBuiltin);
const result = local
.sort((e1, e2) => e1.displayName.localeCompare(e2.displayName))
.filter(e => runningExtensions.some(r => areSameExtensions({ id: r.identifier.value, uuid: r.uuid }, e.identifier))
&& (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)
&& (!categories.length || categories.some(category => (e.local && e.local.manifest.categories || []).some(c => c.toLowerCase() === category))));
return this.sortExtensions(result, options);
}
private mergeAddedExtensions(extensions: IExtension[], newExtensions: IExtension[]): IExtension[] | undefined {
const oldExtensions = [...extensions];
const findPreviousExtensionIndex = (from: number): number => {
let index = -1;
const previousExtensionInNew = newExtensions[from];
if (previousExtensionInNew) {
index = oldExtensions.findIndex(e => areSameExtensions(e.identifier, previousExtensionInNew.identifier));
if (index === -1) {
return findPreviousExtensionIndex(from - 1);
}
}
return index;
};
let hasChanged: boolean = false;
for (let index = 0; index < newExtensions.length; index++) {
const extension = newExtensions[index];
if (extensions.every(r => !areSameExtensions(r.identifier, extension.identifier))) {
hasChanged = true;
extensions.splice(findPreviousExtensionIndex(index - 1) + 1, 0, extension);
}
return this.getPagedModel(result);
}
if (/@outdated/i.test(value)) {
value = value.replace(/@outdated/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();
const local = await this.extensionsWorkbenchService.queryLocal(this.server);
const result = local
.sort((e1, e2) => e1.displayName.localeCompare(e2.displayName))
.filter(extension => extension.outdated
&& (extension.name.toLowerCase().indexOf(value) > -1 || extension.displayName.toLowerCase().indexOf(value) > -1)
&& (!categories.length || categories.some(category => !!extension.local && extension.local.manifest.categories!.some(c => c.toLowerCase() === category))));
return this.getPagedModel(this.sortExtensions(result, options));
}
if (/@disabled/i.test(value)) {
value = value.replace(/@disabled/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();
const local = await this.extensionsWorkbenchService.queryLocal(this.server);
const runningExtensions = await this.extensionService.getExtensions();
const result = local
.sort((e1, e2) => e1.displayName.localeCompare(e2.displayName))
.filter(e => runningExtensions.every(r => !areSameExtensions({ id: r.identifier.value, uuid: r.uuid }, e.identifier))
&& (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)
&& (!categories.length || categories.some(category => (e.local && e.local.manifest.categories || []).some(c => c.toLowerCase() === category))));
return this.getPagedModel(this.sortExtensions(result, options));
}
if (/@enabled/i.test(value)) {
value = value ? value.replace(/@enabled/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase() : '';
const local = (await this.extensionsWorkbenchService.queryLocal(this.server)).filter(e => e.type === ExtensionType.User);
const runningExtensions = await this.extensionService.getExtensions();
const result = local
.sort((e1, e2) => e1.displayName.localeCompare(e2.displayName))
.filter(e => runningExtensions.some(r => areSameExtensions({ id: r.identifier.value, uuid: r.uuid }, e.identifier))
&& (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)
&& (!categories.length || categories.some(category => (e.local && e.local.manifest.categories || []).some(c => c.toLowerCase() === category))));
return this.getPagedModel(this.sortExtensions(result, options));
}
return new PagedModel([]);
return hasChanged ? extensions : undefined;
}
private async queryGallery(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
@@ -601,8 +714,8 @@ export class ExtensionsListView extends ViewPane {
// sort the marketplace extensions
pager.firstPage.sort((a, b) => {
let isRecommendedA: boolean = recommmended.findIndex(ext => ext.extensionId === `${a.publisher}.${a.name}`) > -1;
let isRecommendedB: boolean = recommmended.findIndex(ext => ext.extensionId === `${b.publisher}.${b.name}`) > -1;
let isRecommendedA: boolean = recommmended.findIndex(extensionId => extensionId === `${a.publisher}.${a.name}`) > -1;
let isRecommendedB: boolean = recommmended.findIndex(extensionId => extensionId === `${b.publisher}.${b.name}`) > -1;
// sort recommeded extensions before other extensions
if (isRecommendedA !== isRecommendedB) {
@@ -674,11 +787,10 @@ export class ExtensionsListView extends ViewPane {
return new PagedModel([]);
}
protected async getInstallableRecommendations(recommendations: IExtensionRecommendation[], options: IQueryOptions, token: CancellationToken): Promise<IExtension[]> {
protected async getInstallableRecommendations(recommendations: string[], options: IQueryOptions, token: CancellationToken): Promise<IExtension[]> {
const extensions: IExtension[] = [];
if (recommendations.length) {
const names = recommendations.map(({ extensionId }) => extensionId);
const pager = await this.extensionsWorkbenchService.queryGallery({ ...options, names, pageSize: names.length }, token);
const pager = await this.extensionsWorkbenchService.queryGallery({ ...options, names: recommendations, pageSize: recommendations.length }, token);
for (const extension of pager.firstPage) {
if (extension.gallery && (await this.extensionManagementService.canInstall(extension.gallery))) {
extensions.push(extension);
@@ -688,11 +800,11 @@ export class ExtensionsListView extends ViewPane {
return extensions;
}
protected async getWorkspaceRecommendations(): Promise<IExtensionRecommendation[]> {
protected async getWorkspaceRecommendations(): Promise<string[]> {
const recommendations = await this.extensionRecommendationsService.getWorkspaceRecommendations();
const { important } = await this.extensionRecommendationsService.getConfigBasedRecommendations();
for (const configBasedRecommendation of important) {
if (!recommendations.find(r => r.extensionId === configBasedRecommendation.extensionId)) {
if (!recommendations.find(extensionId => extensionId === configBasedRecommendation)) {
recommendations.push(configBasedRecommendation);
}
}
@@ -705,7 +817,7 @@ export class ExtensionsListView extends ViewPane {
const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-workspace' }, token))
.filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1);
this.telemetryService.publicLog2<{ count: number }, WorkspaceRecommendationsClassification>('extensionWorkspaceRecommendations:open', { count: installableRecommendations.length });
const result: IExtension[] = coalesce(recommendations.map(({ extensionId: id }) => installableRecommendations.find(i => areSameExtensions(i.identifier, { id }))));
const result: IExtension[] = coalesce(recommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id }))));
return new PagedModel(result);
}
@@ -727,11 +839,10 @@ export class ExtensionsListView extends ViewPane {
private async getOtherRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const value = query.value.replace(/@recommended/g, '').trim().toLowerCase();
const local = (await this.extensionsWorkbenchService.queryLocal(this.server))
.filter(e => e.type === ExtensionType.User)
const local = (await this.extensionsWorkbenchService.queryLocal(this.options.server))
.map(e => e.identifier.id.toLowerCase());
const workspaceRecommendations = (await this.getWorkspaceRecommendations())
.map(r => r.extensionId.toLowerCase());
.map(extensionId => extensionId.toLowerCase());
const otherRecommendations = distinct(
flatten(await Promise.all([
@@ -739,21 +850,19 @@ export class ExtensionsListView extends ViewPane {
this.extensionRecommendationsService.getImportantRecommendations(),
this.extensionRecommendationsService.getFileBasedRecommendations(),
this.extensionRecommendationsService.getOtherRecommendations()
])).filter(({ extensionId }) => !local.includes(extensionId.toLowerCase()) && !workspaceRecommendations.includes(extensionId.toLowerCase())
), r => r.extensionId.toLowerCase());
])).filter(extensionId => !local.includes(extensionId.toLowerCase()) && !workspaceRecommendations.includes(extensionId.toLowerCase())
), extensionId => extensionId.toLowerCase());
const installableRecommendations = (await this.getInstallableRecommendations(otherRecommendations, { ...options, source: 'recommendations-other', sortBy: undefined }, token))
.filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1);
const result: IExtension[] = coalesce(otherRecommendations.map(({ extensionId: id }) => installableRecommendations.find(i => areSameExtensions(i.identifier, { id }))));
const result: IExtension[] = coalesce(otherRecommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id }))));
return new PagedModel(result);
}
// Get All types of recommendations, trimmed to show a max of 8 at any given time
private async getAllRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const local = (await this.extensionsWorkbenchService.queryLocal(this.server))
.filter(e => e.type === ExtensionType.User)
.map(e => e.identifier.id.toLowerCase());
const local = (await this.extensionsWorkbenchService.queryLocal(this.options.server)).map(e => e.identifier.id.toLowerCase());
const allRecommendations = distinct(
flatten(await Promise.all([
@@ -762,11 +871,11 @@ export class ExtensionsListView extends ViewPane {
this.extensionRecommendationsService.getImportantRecommendations(),
this.extensionRecommendationsService.getFileBasedRecommendations(),
this.extensionRecommendationsService.getOtherRecommendations()
])).filter(({ extensionId }) => !local.includes(extensionId.toLowerCase())
), r => r.extensionId.toLowerCase());
])).filter(extensionId => !local.includes(extensionId.toLowerCase())
), extensionId => extensionId.toLowerCase());
const installableRecommendations = await this.getInstallableRecommendations(allRecommendations, { ...options, source: 'recommendations-all', sortBy: undefined }, token);
const result: IExtension[] = coalesce(allRecommendations.map(({ extensionId: id }) => installableRecommendations.find(i => areSameExtensions(i.identifier, { id }))));
const result: IExtension[] = coalesce(allRecommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id }))));
return new PagedModel(result.slice(0, 8));
}
@@ -782,31 +891,51 @@ export class ExtensionsListView extends ViewPane {
if (this.list) {
this.list.model = new DelayedPagedModel(model);
this.list.scrollTop = 0;
const count = this.count();
this.updateBody(error);
}
}
if (this.bodyTemplate && this.badge) {
private updateBody(error?: any): void {
const count = this.count();
if (this.bodyTemplate && this.badge) {
this.bodyTemplate.extensionsList.classList.toggle('hidden', count === 0);
this.bodyTemplate.messageContainer.classList.toggle('hidden', count > 0);
this.badge.setCount(count);
this.bodyTemplate.extensionsList.classList.toggle('hidden', count === 0);
this.bodyTemplate.messageContainer.classList.toggle('hidden', count > 0);
this.badge.setCount(count);
if (count === 0 && this.isBodyVisible()) {
if (error) {
if (error instanceof ExtensionListViewWarning) {
this.bodyTemplate.messageSeverityIcon.className = `codicon ${SeverityIcon.className(Severity.Warning)}`;
this.bodyTemplate.messageBox.textContent = getErrorMessage(error);
} else {
this.bodyTemplate.messageSeverityIcon.className = `codicon ${SeverityIcon.className(Severity.Error)}`;
this.bodyTemplate.messageBox.textContent = localize('error', "Error while loading extensions. {0}", getErrorMessage(error));
}
if (count === 0 && this.isBodyVisible()) {
if (error) {
if (error instanceof ExtensionListViewWarning) {
this.bodyTemplate.messageSeverityIcon.className = SeverityIcon.className(Severity.Warning);
this.bodyTemplate.messageBox.textContent = getErrorMessage(error);
} else {
this.bodyTemplate.messageSeverityIcon.className = '';
this.bodyTemplate.messageBox.textContent = localize('no extensions found', "No extensions found.");
this.bodyTemplate.messageSeverityIcon.className = SeverityIcon.className(Severity.Error);
this.bodyTemplate.messageBox.textContent = localize('error', "Error while loading extensions. {0}", getErrorMessage(error));
}
alert(this.bodyTemplate.messageBox.textContent);
} else {
this.bodyTemplate.messageSeverityIcon.className = '';
this.bodyTemplate.messageBox.textContent = localize('no extensions found', "No extensions found.");
}
alert(this.bodyTemplate.messageBox.textContent);
}
}
this.updateSize();
}
protected updateSize() {
if (this.options.fixedHeight) {
const length = this.list?.model.length || 0;
this.minimumBodySize = Math.min(length, 3) * EXTENSION_LIST_ELEMENT_HEIGHT;
this.maximumBodySize = length * EXTENSION_LIST_ELEMENT_HEIGHT;
this.storageService.store(this.id, this.maximumBodySize, StorageScope.GLOBAL, StorageTarget.MACHINE);
}
}
private updateModel(model: IPagedModel<IExtension>) {
if (this.list) {
this.list.model = new DelayedPagedModel(model);
this.updateBody();
}
}
private openExtension(extension: IExtension, options: { sideByside?: boolean, preserveFocus?: boolean, pinned?: boolean }): void {
@@ -854,6 +983,10 @@ export class ExtensionsListView extends ViewPane {
this.queryRequest.request.cancel();
this.queryRequest = null;
}
if (this.queryResult) {
this.queryResult.disposables.dispose();
this.queryResult = undefined;
}
this.list = null;
}
@@ -863,7 +996,8 @@ export class ExtensionsListView extends ViewPane {
|| this.isEnabledExtensionsQuery(query)
|| this.isDisabledExtensionsQuery(query)
|| this.isBuiltInExtensionsQuery(query)
|| this.isSearchBuiltInExtensionsQuery(query);
|| this.isSearchBuiltInExtensionsQuery(query)
|| this.isBuiltInGroupExtensionsQuery(query);
}
static isSearchBuiltInExtensionsQuery(query: string): boolean {
@@ -871,7 +1005,11 @@ export class ExtensionsListView extends ViewPane {
}
static isBuiltInExtensionsQuery(query: string): boolean {
return /@builtin$/i.test(query.trim());
return /^\s*@builtin$/i.test(query.trim());
}
static isBuiltInGroupExtensionsQuery(query: string): boolean {
return /^\s*@builtin:.+$/i.test(query.trim());
}
static isInstalledExtensionsQuery(query: string): boolean {
@@ -928,39 +1066,7 @@ export class ExtensionsListView extends ViewPane {
}
}
export class ServerExtensionsView extends ExtensionsListView {
constructor(
server: IExtensionManagementServer,
onDidChangeTitle: Event<string>,
options: ExtensionsListViewOptions,
@INotificationService notificationService: INotificationService,
@IKeybindingService keybindingService: IKeybindingService,
@IContextMenuService contextMenuService: IContextMenuService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IInstantiationService instantiationService: IInstantiationService,
@IExtensionService extensionService: IExtensionService,
@IExtensionRecommendationsService tipsService: IExtensionRecommendationsService,
@ITelemetryService telemetryService: ITelemetryService,
@IConfigurationService configurationService: IConfigurationService,
@IWorkspaceContextService contextService: IWorkspaceContextService,
@IExperimentService experimentService: IExperimentService,
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService,
@IExtensionManagementService extensionManagementService: IExtensionManagementService,
@IProductService productService: IProductService,
@IContextKeyService contextKeyService: IContextKeyService,
@IMenuService menuService: IMenuService,
@IOpenerService openerService: IOpenerService,
@IThemeService themeService: IThemeService,
@IPreferencesService preferencesService: IPreferencesService,
) {
options.server = server;
super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, tipsService,
telemetryService, configurationService, contextService, experimentService, extensionManagementServerService, extensionManagementService, productService,
contextKeyService, viewDescriptorService, menuService, openerService, preferencesService);
this._register(onDidChangeTitle(title => this.updateTitle(title)));
}
export class ServerInstalledExtensionsView extends ExtensionsListView {
async show(query: string): Promise<IPagedModel<IExtension>> {
query = query ? query : '@installed';
@@ -971,9 +1077,9 @@ export class ServerExtensionsView extends ExtensionsListView {
}
getActions(): IAction[] {
if (this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManagementServerService.localExtensionManagementServer === this.server) {
if (this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManagementServerService.localExtensionManagementServer === this.options.server) {
const installLocalExtensionsInRemoteAction = this._register(this.instantiationService.createInstance(InstallLocalExtensionsInRemoteAction));
installLocalExtensionsInRemoteAction.class = 'codicon codicon-cloud-download';
installLocalExtensionsInRemoteAction.class = ThemeIcon.asClassName(installLocalInRemoteIcon);
return [installLocalExtensionsInRemoteAction];
}
return [];
@@ -996,28 +1102,6 @@ export class DisabledExtensionsView extends ExtensionsListView {
}
}
export class OutdatedExtensionsView extends ExtensionsListView {
async show(query: string): Promise<IPagedModel<IExtension>> {
query = query || '@outdated';
return ExtensionsListView.isOutdatedExtensionsQuery(query) ? super.show(query) : this.showEmptyModel();
}
}
export class InstalledExtensionsView extends ExtensionsListView {
async show(query: string): Promise<IPagedModel<IExtension>> {
query = query || '@installed';
return ExtensionsListView.isInstalledExtensionsQuery(query) ? super.show(query) : this.showEmptyModel();
}
}
export class SearchBuiltInExtensionsView extends ExtensionsListView {
async show(query: string): Promise<IPagedModel<IExtension>> {
return ExtensionsListView.isSearchBuiltInExtensionsQuery(query) ? super.show(query) : this.showEmptyModel();
}
}
export class BuiltInFeatureExtensionsView extends ExtensionsListView {
async show(query: string): Promise<IPagedModel<IExtension>> {
return (query && query.trim() !== '@builtin') ? this.showEmptyModel() : super.show('@builtin:features');
@@ -1043,7 +1127,7 @@ export class DefaultRecommendedExtensionsView extends ExtensionsListView {
renderBody(container: HTMLElement): void {
super.renderBody(container);
this._register(this.extensionRecommendationsService.onRecommendationChange(() => {
this._register(this.extensionRecommendationsService.onDidChangeRecommendations(() => {
this.show('');
}));
}
@@ -1053,7 +1137,7 @@ export class DefaultRecommendedExtensionsView extends ExtensionsListView {
return this.showEmptyModel();
}
const model = await super.show(this.recommendedExtensionsQuery);
if (!this.extensionsWorkbenchService.local.some(e => e.type === ExtensionType.User)) {
if (!this.extensionsWorkbenchService.local.some(e => !e.isBuiltin)) {
// This is part of popular extensions view. Collapse if no installed extensions.
this.setExpanded(model.length > 0);
}
@@ -1068,7 +1152,7 @@ export class RecommendedExtensionsView extends ExtensionsListView {
renderBody(container: HTMLElement): void {
super.renderBody(container);
this._register(this.extensionRecommendationsService.onRecommendationChange(() => {
this._register(this.extensionRecommendationsService.onDidChangeRecommendations(() => {
this.show('');
}));
}
@@ -1086,17 +1170,17 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView {
renderBody(container: HTMLElement): void {
super.renderBody(container);
this._register(this.extensionRecommendationsService.onRecommendationChange(() => this.show(this.recommendedExtensionsQuery)));
this._register(this.extensionRecommendationsService.onDidChangeRecommendations(() => this.show(this.recommendedExtensionsQuery)));
this._register(this.contextService.onDidChangeWorkbenchState(() => this.show(this.recommendedExtensionsQuery)));
}
getActions(): IAction[] {
if (!this.installAllAction) {
this.installAllAction = this._register(new Action('workbench.extensions.action.installWorkspaceRecommendedExtensions', localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), 'codicon codicon-cloud-download', false, () => this.installWorkspaceRecommendations()));
this.installAllAction = this._register(new Action('workbench.extensions.action.installWorkspaceRecommendedExtensions', localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), ThemeIcon.asClassName(installWorkspaceRecommendedIcon), false, () => this.installWorkspaceRecommendations()));
}
const configureWorkspaceFolderAction = this._register(this.instantiationService.createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL));
configureWorkspaceFolderAction.class = 'codicon codicon-pencil';
configureWorkspaceFolderAction.class = ThemeIcon.asClassName(configureRecommendedIcon);
return [this.installAllAction, configureWorkspaceFolderAction];
}
@@ -1119,13 +1203,13 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView {
const installed = (await this.extensionsWorkbenchService.queryLocal())
.filter(l => l.enablementState !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind
const recommendations = (await this.getWorkspaceRecommendations())
.filter(({ extensionId }) => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier)));
.filter(extensionId => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier)));
return this.getInstallableRecommendations(recommendations, { source: 'install-all-workspace-recommendations' }, CancellationToken.None);
}
private async installWorkspaceRecommendations(): Promise<void> {
const installableRecommendations = await this.getInstallableWorkspaceRecommendations();
await Promise.all(installableRecommendations.map(extension => this.extensionManagementService.installFromGallery(extension.gallery!)));
await this.extensionManagementService.installExtensions(installableRecommendations.map(i => i.gallery!));
}
}

View File

@@ -5,18 +5,22 @@
import 'vs/css!./media/extensionsWidgets';
import { Disposable, toDisposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
import { IExtension, IExtensionsWorkbenchService, IExtensionContainer } from 'vs/workbench/contrib/extensions/common/extensions';
import { IExtension, IExtensionsWorkbenchService, IExtensionContainer, ExtensionState } from 'vs/workbench/contrib/extensions/common/extensions';
import { append, $ } from 'vs/base/browser/dom';
import * as platform from 'vs/base/common/platform';
import { localize } from 'vs/nls';
import { IExtensionRecommendationsService, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { ILabelService } from 'vs/platform/label/common/label';
import { extensionButtonProminentBackground, extensionButtonProminentForeground, ExtensionToolTipAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { IThemeService, IColorTheme } from 'vs/platform/theme/common/themeService';
import { IThemeService, IColorTheme, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { EXTENSION_BADGE_REMOTE_BACKGROUND, EXTENSION_BADGE_REMOTE_FOREGROUND } from 'vs/workbench/common/theme';
import { Emitter, Event } from 'vs/base/common/event';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
import { installCountIcon, ratingIcon, remoteIcon, starEmptyIcon, starFullIcon, starHalfIcon, syncIgnoredIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons';
export abstract class ExtensionWidget extends Disposable implements IExtensionContainer {
private _extension: IExtension | null = null;
@@ -82,7 +86,7 @@ export class InstallCountWidget extends ExtensionWidget {
installLabel = installCount.toLocaleString(platform.locale);
}
append(this.container, $('span.codicon.codicon-cloud-download'));
append(this.container, $('span' + ThemeIcon.asCSSSelector(installCountIcon)));
const count = append(this.container, $('span.count'));
count.textContent = installLabel;
}
@@ -122,18 +126,18 @@ export class RatingsWidget extends ExtensionWidget {
const rating = Math.round(this.extension.rating * 2) / 2;
if (this.small) {
append(this.container, $('span.codicon.codicon-star-full'));
append(this.container, $('span' + ThemeIcon.asCSSSelector(starFullIcon)));
const count = append(this.container, $('span.count'));
count.textContent = String(rating);
} else {
for (let i = 1; i <= 5; i++) {
if (rating >= i) {
append(this.container, $('span.codicon.codicon-star-full'));
append(this.container, $('span' + ThemeIcon.asCSSSelector(starFullIcon)));
} else if (rating >= i - 0.5) {
append(this.container, $('span.codicon.codicon-star-half'));
append(this.container, $('span' + ThemeIcon.asCSSSelector(starHalfIcon)));
} else {
append(this.container, $('span.codicon.codicon-star-empty'));
append(this.container, $('span' + ThemeIcon.asCSSSelector(starEmptyIcon)));
}
}
}
@@ -198,7 +202,7 @@ export class RecommendationWidget extends ExtensionWidget {
super();
this.render();
this._register(toDisposable(() => this.clear()));
this._register(this.extensionRecommendationsService.onRecommendationChange(() => this.render()));
this._register(this.extensionRecommendationsService.onDidChangeRecommendations(() => this.render()));
}
private clear(): void {
@@ -219,7 +223,7 @@ export class RecommendationWidget extends ExtensionWidget {
if (extRecommendations[this.extension.identifier.id.toLowerCase()]) {
this.element = append(this.parent, $('div.extension-bookmark'));
const recommendation = append(this.element, $('.recommendation'));
append(recommendation, $('span.codicon.codicon-star'));
append(recommendation, $('span' + ThemeIcon.asCSSSelector(ratingIcon)));
const applyBookmarkStyle = (theme: IColorTheme) => {
const bgColor = theme.getColor(extensionButtonProminentBackground);
const fgColor = theme.getColor(extensionButtonProminentForeground);
@@ -285,7 +289,7 @@ class RemoteBadge extends Disposable {
}
private render(): void {
append(this.element, $('span.codicon.codicon-remote'));
append(this.element, $('span' + ThemeIcon.asCSSSelector(remoteIcon)));
const applyBadgeStyle = () => {
if (!this.element) {
@@ -339,3 +343,28 @@ export class ExtensionPackCountWidget extends ExtensionWidget {
countBadge.setCount(this.extension.extensionPack.length);
}
}
export class SyncIgnoredWidget extends ExtensionWidget {
private element: HTMLElement;
constructor(
container: HTMLElement,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService,
) {
super();
this.element = append(container, $('span.extension-sync-ignored' + ThemeIcon.asCSSSelector(syncIgnoredIcon)));
this.element.title = localize('syncingore.label', "This extension is ignored during sync.");
this.element.classList.add(...ThemeIcon.asClassNameArray(syncIgnoredIcon));
this.element.classList.add('hide');
this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectedKeys.includes('settingsSync.ignoredExtensions'))(() => this.render()));
this._register(userDataAutoSyncEnablementService.onDidChangeEnablement(() => this.update()));
this.render();
}
render(): void {
this.element.classList.toggle('hide', !(this.extension && this.extension.state === ExtensionState.Installed && this.userDataAutoSyncEnablementService.isEnabled() && this.extensionsWorkbenchService.isExtensionIgnoredToSync(this.extension)));
}
}

View File

@@ -4,23 +4,23 @@
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as semver from 'semver-umd';
import * as semver from 'vs/base/common/semver/semver';
import { Event, Emitter } from 'vs/base/common/event';
import { index, distinct } from 'vs/base/common/arrays';
import { ThrottledDelayer } from 'vs/base/common/async';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { canceled, isPromiseCanceledError } from 'vs/base/common/errors';
import { Disposable } from 'vs/base/common/lifecycle';
import { IPager, mapPager, singlePagePager } from 'vs/base/common/paging';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
// {{SQL CARBON EDIT}}
import {
IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions,
InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, IExtensionIdentifier, InstallOperation, DefaultIconPath
InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, IExtensionIdentifier, InstallOperation, DefaultIconPath, InstallOptions
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensioManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, getMaliciousExtensionsSet, groupByExtension, ExtensionIdentifierWithVersion, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { URI } from 'vs/base/common/uri';
import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions';
@@ -29,18 +29,18 @@ import { IURLService, IURLHandler, IOpenURLOptions } from 'vs/platform/url/commo
import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import { ILogService } from 'vs/platform/log/common/log';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import * as resources from 'vs/base/common/resources';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { IFileService } from 'vs/platform/files/common/files';
import { IExtensionManifest, ExtensionType, IExtension as IPlatformExtension, isLanguagePackExtension, ExtensionsPolicyKey, ExtensionsPolicy } from 'vs/platform/extensions/common/extensions'; // {{SQL CARBON EDIT}}
import { IExtensionManifest, ExtensionType, IExtension as IPlatformExtension, ExtensionsPolicyKey, ExtensionsPolicy } from 'vs/platform/extensions/common/extensions'; // {{SQL CARBON EDIT}}
import { IModeService } from 'vs/editor/common/services/modeService';
import { IProductService } from 'vs/platform/product/common/productService';
import { asDomUri } from 'vs/base/browser/dom';
import { getIgnoredExtensions } from 'vs/platform/userDataSync/common/extensionsMerge';
import { isWeb } from 'vs/base/common/platform';
import { getExtensionKind } from 'vs/workbench/services/extensions/common/extensionsUtil';
import { FileAccess } from 'vs/base/common/network';
import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions';
import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync';
import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; // {{SQL CARBON EDIT}}
import { IOpenerService } from 'vs/platform/opener/common/opener'; // {{SQL CARBON EDIT}}
@@ -69,6 +69,10 @@ class Extension implements IExtension {
return this.local ? this.local.type : ExtensionType.User;
}
get isBuiltin(): boolean {
return this.local ? this.local.isBuiltin : false;
}
get name(): string {
return this.gallery ? this.gallery.name : this.local!.manifest.name;
}
@@ -151,7 +155,7 @@ class Extension implements IExtension {
private get localIconUrl(): string | null {
if (this.local && this.local.manifest.icon) {
return asDomUri(resources.joinPath(this.local.location, this.local.manifest.icon)).toString(true);
return FileAccess.asBrowserUri(resources.joinPath(this.local.location, this.local.manifest.icon)).toString(true);
}
return null;
}
@@ -168,10 +172,10 @@ class Extension implements IExtension {
if (this.type === ExtensionType.System && this.local) {
if (this.local.manifest && this.local.manifest.contributes) {
if (Array.isArray(this.local.manifest.contributes.themes) && this.local.manifest.contributes.themes.length) {
return require.toUrl('./media/theme-icon.png');
return FileAccess.asBrowserUri('./media/theme-icon.png', require).toString(true);
}
if (Array.isArray(this.local.manifest.contributes.grammars) && this.local.manifest.contributes.grammars.length) {
return require.toUrl('./media/language-icon.svg');
return FileAccess.asBrowserUri('./media/language-icon.svg', require).toString(true);
}
}
}
@@ -222,12 +226,12 @@ class Extension implements IExtension {
return this.gallery ? this.gallery.preview : false;
}
private isGalleryOutdated(): boolean {
return this.local && this.gallery ? semver.gt(this.local.manifest.version, this.gallery.version) : false;
}
getManifest(token: CancellationToken): Promise<IExtensionManifest | null> {
if (this.gallery && !this.isGalleryOutdated()) {
if (this.local && !this.outdated) {
return Promise.resolve(this.local.manifest);
}
if (this.gallery) {
if (this.gallery.assets.manifest) {
return this.galleryService.getManifest(this.gallery, token);
}
@@ -235,19 +239,15 @@ class Extension implements IExtension {
return Promise.resolve(null);
}
if (this.local) {
return Promise.resolve(this.local.manifest);
}
return Promise.resolve(null);
}
hasReadme(): boolean {
if (this.gallery && !this.isGalleryOutdated() && this.gallery.assets.readme) {
if (this.local && this.local.readmeUrl) {
return true;
}
if (this.local && this.local.readmeUrl) {
if (this.gallery && this.gallery.assets.readme) {
return true;
}
@@ -255,17 +255,17 @@ class Extension implements IExtension {
}
getReadme(token: CancellationToken): Promise<string> {
if (this.gallery && !this.isGalleryOutdated()) {
if (this.local && this.local.readmeUrl && !this.outdated) {
return this.fileService.readFile(this.local.readmeUrl).then(content => content.value.toString());
}
if (this.gallery) {
if (this.gallery.assets.readme) {
return this.galleryService.getReadme(this.gallery, token);
}
this.telemetryService.publicLog('extensions:NotFoundReadMe', this.telemetryData);
}
if (this.local && this.local.readmeUrl) {
return this.fileService.readFile(this.local.readmeUrl).then(content => content.value.toString());
}
if (this.type === ExtensionType.System) {
// {{SQL CARBON EDIT}}
return Promise.resolve(`# ${this.displayName || this.name}
@@ -279,11 +279,11 @@ ${this.description}
}
hasChangelog(): boolean {
if (this.gallery && this.gallery.assets.changelog && !this.isGalleryOutdated()) {
if (this.local && this.local.changelogUrl) {
return true;
}
if (this.local && this.local.changelogUrl) {
if (this.gallery && this.gallery.assets.changelog) {
return true;
}
@@ -291,7 +291,12 @@ ${this.description}
}
getChangelog(token: CancellationToken): Promise<string> {
if (this.gallery && this.gallery.assets.changelog && !this.isGalleryOutdated()) {
if (this.local && this.local.changelogUrl && !this.outdated) {
return this.fileService.readFile(this.local.changelogUrl).then(content => content.value.toString());
}
if (this.gallery && this.gallery.assets.changelog) {
return this.galleryService.getChangelog(this.gallery, token);
}
@@ -311,23 +316,23 @@ ${this.description}
get dependencies(): string[] {
const { local, gallery } = this;
if (gallery && !this.isGalleryOutdated()) {
return gallery.properties.dependencies || [];
}
if (local && local.manifest.extensionDependencies) {
if (local && local.manifest.extensionDependencies && !this.outdated) {
return local.manifest.extensionDependencies;
}
if (gallery) {
return gallery.properties.dependencies || [];
}
return [];
}
get extensionPack(): string[] {
const { local, gallery } = this;
if (gallery && !this.isGalleryOutdated()) {
return gallery.properties.extensionPack || [];
}
if (local && local.manifest.extensionPack) {
if (local && local.manifest.extensionPack && !this.outdated) {
return local.manifest.extensionPack;
}
if (gallery) {
return gallery.properties.extensionPack || [];
}
return [];
}
}
@@ -526,7 +531,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IEditorService private readonly editorService: IEditorService,
@IExtensionManagementService private readonly extensionService: IExtensionManagementService,
@IWorkbenchExtensioManagementService private readonly extensionManagementService: IWorkbenchExtensioManagementService,
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@@ -538,8 +543,11 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
@IStorageService private readonly storageService: IStorageService,
@IModeService private readonly modeService: IModeService,
@IIgnoredExtensionsManagementService private readonly extensionsSyncManagementService: IIgnoredExtensionsManagementService,
@IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService,
@IProductService private readonly productService: IProductService,
@IOpenerService private readonly openerService: IOpenerService // {{SQL CARBON EDIT}}
@IOpenerService private readonly openerService: IOpenerService, // {{SQL CARBON EDIT}}
@IExtensionManagementService private readonly extensionService: IExtensionManagementService
) {
super();
if (extensionManagementServerService.localExtensionManagementServer) {
@@ -646,7 +654,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
const options: IQueryOptions = CancellationToken.isCancellationToken(arg1) ? {} : arg1;
const token: CancellationToken = CancellationToken.isCancellationToken(arg1) ? arg1 : arg2;
options.text = options.text ? this.resolveQueryText(options.text) : options.text;
return this.extensionService.getExtensionsReport()
return this.extensionManagementService.getExtensionsReport()
.then(report => {
const maliciousSet = getMaliciousExtensionsSet(report);
@@ -921,7 +929,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
return false;
}
install(extension: URI | IExtension): Promise<IExtension> {
install(extension: URI | IExtension, installOptions?: InstallOptions): Promise<IExtension> {
let extensionPolicy = this.configurationService.getValue<string>(ExtensionsPolicyKey); // {{SQL CARBON EDIT}} add line
if (extension instanceof URI) {
return this.installWithProgress(() => this.installFromVSIX(extension));
@@ -986,7 +994,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
location: ProgressLocation.Extensions,
title: nls.localize('uninstallingExtension', 'Uninstalling extension....'),
source: `${toUninstall.identifier.id}`
}, () => this.extensionService.uninstall(toUninstall).then(() => undefined));
}, () => this.extensionManagementService.uninstall(toUninstall).then(() => undefined));
}
installVersion(extension: IExtension, version: string): Promise<IExtension> {
@@ -1025,38 +1033,44 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
return this.progressService.withProgress({
location: ProgressLocation.Extensions,
source: `${toReinstall.identifier.id}`
}, () => this.extensionService.reinstallFromGallery(toReinstall).then(() => this.local.filter(local => areSameExtensions(local.identifier, extension.identifier))[0]));
}, () => this.extensionManagementService.reinstallFromGallery(toReinstall).then(() => this.local.filter(local => areSameExtensions(local.identifier, extension.identifier))[0]));
}
isExtensionIgnoredToSync(extension: IExtension): boolean {
const localExtensions = (!isWeb && this.localExtensions ? this.localExtensions.local : this.local)
.filter(l => !!l.local)
.map(l => l.local!);
const ignoredExtensions = getIgnoredExtensions(localExtensions, this.configurationService);
return ignoredExtensions.includes(extension.identifier.id.toLowerCase());
return extension.local ? !this.isInstalledExtensionSynced(extension.local)
: this.extensionsSyncManagementService.hasToNeverSyncExtension(extension.identifier.id);
}
toggleExtensionIgnoredToSync(extension: IExtension): Promise<void> {
async toggleExtensionIgnoredToSync(extension: IExtension): Promise<void> {
const isIgnored = this.isExtensionIgnoredToSync(extension);
const isDefaultIgnored = extension.local?.isMachineScoped;
const id = extension.identifier.id.toLowerCase();
// first remove the extension completely from ignored extensions
let currentValue = [...this.configurationService.getValue<string[]>('settingsSync.ignoredExtensions')].map(id => id.toLowerCase());
currentValue = currentValue.filter(v => v !== id && v !== `-${id}`);
// If ignored, then add only if it is ignored by default
if (isIgnored && isDefaultIgnored) {
currentValue.push(`-${id}`);
if (extension.local && isIgnored) {
(<Extension>extension).local = await this.updateSynchronizingInstalledExtension(extension.local, true);
this._onChange.fire(extension);
} else {
this.extensionsSyncManagementService.updateIgnoredExtensions(extension.identifier.id, !isIgnored);
}
await this.userDataAutoSyncService.triggerSync(['IgnoredExtensionsUpdated'], false, false);
}
// If asked not to sync, then add only if it is not ignored by default
if (!isIgnored && !isDefaultIgnored) {
currentValue.push(id);
private isInstalledExtensionSynced(extension: ILocalExtension): boolean {
if (extension.isMachineScoped) {
return false;
}
if (this.extensionsSyncManagementService.hasToAlwaysSyncExtension(extension.identifier.id)) {
return true;
}
return !this.extensionsSyncManagementService.hasToNeverSyncExtension(extension.identifier.id);
}
return this.configurationService.updateValue('settingsSync.ignoredExtensions', currentValue.length ? currentValue : undefined, ConfigurationTarget.USER);
async updateSynchronizingInstalledExtension(extension: ILocalExtension, sync: boolean): Promise<ILocalExtension> {
const isMachineScoped = !sync;
if (extension.isMachineScoped !== isMachineScoped) {
extension = await this.extensionManagementService.updateExtensionScope(extension, isMachineScoped);
}
if (sync) {
this.extensionsSyncManagementService.updateIgnoredExtensions(extension.identifier.id, false);
}
return extension;
}
private installWithProgress<T>(installTask: () => Promise<T>, extensionName?: string): Promise<T> {
@@ -1068,9 +1082,9 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
}
private async installFromVSIX(vsix: URI): Promise<IExtension> {
const manifest = await this.extensionService.getManifest(vsix);
const manifest = await this.extensionManagementService.getManifest(vsix);
const existingExtension = this.local.find(local => areSameExtensions(local.identifier, { id: getGalleryExtensionId(manifest.publisher, manifest.name) }));
const { identifier } = await this.extensionService.install(vsix);
const { identifier } = await this.extensionManagementService.install(vsix);
if (existingExtension && existingExtension.latestVersion !== manifest.version) {
this.ignoreAutoUpdate(new ExtensionIdentifierWithVersion(identifier, manifest.version));
@@ -1079,12 +1093,15 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
return this.local.filter(local => areSameExtensions(local.identifier, identifier))[0];
}
private async installFromGallery(extension: IExtension, gallery: IGalleryExtension): Promise<IExtension> {
private async installFromGallery(extension: IExtension, gallery: IGalleryExtension, installOptions?: InstallOptions): Promise<IExtension> {
this.installing.push(extension);
this._onChange.fire(extension);
try {
const extensionService = extension.server && extension.local && !isLanguagePackExtension(extension.local.manifest) ? extension.server.extensionManagementService : this.extensionService;
await extensionService.installFromGallery(gallery);
if (extension.state === ExtensionState.Installed && extension.local) {
await this.extensionManagementService.updateFromGallery(gallery, extension.local);
} else {
await this.extensionManagementService.installFromGallery(gallery, installOptions);
}
const ids: string[] | undefined = extension.identifier.uuid ? [extension.identifier.uuid] : undefined;
const names: string[] | undefined = extension.identifier.uuid ? undefined : [extension.identifier.id];
this.queryGallery({ names, ids, pageSize: 1 }, CancellationToken.None);
@@ -1116,7 +1133,23 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
for (const extension of extensions) {
let dependents = this.getDependentsAfterDisablement(extension, allExtensions, this.local);
if (dependents.length) {
return Promise.reject(new Error(this.getDependentsErrorMessage(extension, allExtensions, dependents)));
return new Promise<void>((resolve, reject) => {
this.notificationService.prompt(Severity.Error, this.getDependentsErrorMessage(extension, allExtensions, dependents), [
{
label: nls.localize('disable all', 'Disable All'),
run: async () => {
try {
await this.checkAndSetEnablement(dependents, [extension], enablementState);
resolve();
} catch (error) {
reject(error);
}
}
}
], {
onCancel: () => reject(canceled())
});
});
}
}
}
@@ -1137,7 +1170,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
return false;
}
const enable = enablementState === EnablementState.EnabledGlobally || enablementState === EnablementState.EnabledWorkspace;
return (enable || i.type === ExtensionType.User) // Include all Extensions for enablement and only user extensions for disablement
return (enable || !i.isBuiltin) // Include all Extensions for enablement and only non builtin extensions for disablement
&& (options.dependencies || options.pack)
&& extensions.some(extension =>
(options.dependencies && extension.dependencies.some(id => areSameExtensions({ id }, i.identifier)))
@@ -1182,13 +1215,13 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
private getErrorMessageForDisablingAnExtensionWithDependents(extension: IExtension, dependents: IExtension[]): string {
if (dependents.length === 1) {
return nls.localize('singleDependentError', "Cannot disable extension '{0}'. Extension '{1}' depends on this.", extension.displayName, dependents[0].displayName);
return nls.localize('singleDependentError', "Cannot disable '{0}' extension alone. '{1}' extension depends on this. Do you want to disable all these extensions?", extension.displayName, dependents[0].displayName);
}
if (dependents.length === 2) {
return nls.localize('twoDependentsError', "Cannot disable extension '{0}'. Extensions '{1}' and '{2}' depend on this.",
return nls.localize('twoDependentsError', "Cannot disable '{0}' extension alone. '{1}' and '{2}' extensions depend on this. Do you want to disable all these extensions?",
extension.displayName, dependents[0].displayName, dependents[1].displayName);
}
return nls.localize('multipleDependentsError', "Cannot disable extension '{0}'. Extensions '{1}', '{2}' and others depend on this.",
return nls.localize('multipleDependentsError', "Cannot disable '{0}' extension alone. '{1}', '{2}' and other extensions depend on this. Do you want to disable all these extensions?",
extension.displayName, dependents[0].displayName, dependents[1].displayName);
}
@@ -1296,7 +1329,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
private set ignoredAutoUpdateExtensions(extensionIds: string[]) {
this._ignoredAutoUpdateExtensions = distinct(extensionIds.map(id => id.toLowerCase()));
this.storageService.store('extensions.ignoredAutoUpdateExtension', JSON.stringify(this._ignoredAutoUpdateExtensions), StorageScope.GLOBAL);
this.storageService.store('extensions.ignoredAutoUpdateExtension', JSON.stringify(this._ignoredAutoUpdateExtensions), StorageScope.GLOBAL, StorageTarget.MACHINE);
}
private ignoreAutoUpdate(identifierWithVersion: ExtensionIdentifierWithVersion): void {

View File

@@ -4,13 +4,14 @@
*--------------------------------------------------------------------------------------------*/
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { ExtensionRecommendationSource, ExtensionRecommendationReason, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { IExtensionsViewPaneContainer, IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions';
import { CancellationToken } from 'vs/base/common/cancellation';
import { localize } from 'vs/nls';
import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage';
import { StorageScope, IStorageService, StorageTarget } from 'vs/platform/storage/common/storage';
import { ImportantExtensionTip, IProductService } from 'vs/platform/product/common/productService';
import { forEach, IStringDictionary } from 'vs/base/common/collections';
import { ITextModel } from 'vs/editor/common/model';
@@ -24,12 +25,17 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IModelService } from 'vs/editor/common/services/modelService';
import { setImmediate } from 'vs/base/common/platform';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
import { distinct } from 'vs/base/common/arrays';
import { DisposableStore } from 'vs/base/common/lifecycle';
type FileExtensionSuggestionClassification = {
userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
fileExtension: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
};
const promptedRecommendationsStorageKey = 'fileBasedRecommendations/promptedRecommendations';
const promptedFileExtensionsStorageKey = 'fileBasedRecommendations/promptedFileExtensions';
const recommendationsStorageKey = 'extensionsAssistant/recommendations';
const searchMarketplace = localize('searchMarketplace', "Search Marketplace");
const milliSecondsInADay = 1000 * 60 * 60 * 24;
@@ -41,7 +47,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
private readonly fileBasedRecommendationsByPattern = new Map<string, string[]>();
private readonly fileBasedRecommendationsByLanguage = new Map<string, string[]>();
private readonly fileBasedRecommendations = new Map<string, { recommendedTime: number, sources: ExtensionRecommendationSource[] }>();
private readonly fileBasedRecommendations = new Map<string, { recommendedTime: number }>();
private readonly processedFileExtensions: string[] = [];
private readonly processedLanguages: string[] = [];
@@ -60,16 +66,13 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
return this.fileBasedRecommendations.get(a)!.recommendedTime > this.fileBasedRecommendations.get(b)!.recommendedTime ? -1 : 1;
})
.forEach(extensionId => {
for (const source of this.fileBasedRecommendations.get(extensionId)!.sources) {
recommendations.push({
extensionId,
source,
reason: {
reasonId: ExtensionRecommendationReason.File,
reasonText: localize('fileBasedRecommendation', "This extension is recommended based on the files you recently opened.")
}
});
}
recommendations.push({
extensionId,
reason: {
reasonId: ExtensionRecommendationReason.File,
reasonText: localize('fileBasedRecommendation', "This extension is recommended based on the files you recently opened.")
}
});
});
return recommendations;
}
@@ -83,7 +86,6 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
}
constructor(
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionService private readonly extensionService: IExtensionService,
@IViewletService private readonly viewletService: IViewletService,
@@ -93,8 +95,10 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
@INotificationService private readonly notificationService: INotificationService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IStorageService private readonly storageService: IStorageService,
@IExtensionRecommendationNotificationService private readonly extensionRecommendationNotificationService: IExtensionRecommendationNotificationService,
@IExtensionIgnoredRecommendationsService private readonly extensionIgnoredRecommendationsService: IExtensionIgnoredRecommendationsService,
) {
super(promptedExtensionRecommendations);
super();
if (productService.extensionTips) {
forEach(productService.extensionTips, ({ key, value }) => this.extensionTips.set(key.toLowerCase(), value));
@@ -138,7 +142,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
forEach(cachedRecommendations, ({ key, value }) => {
const diff = (now - value) / milliSecondsInADay;
if (diff <= 7 && allRecommendations.indexOf(key) > -1) {
this.fileBasedRecommendations.set(key.toLowerCase(), { recommendedTime: value, sources: ['cached'] });
this.fileBasedRecommendations.set(key.toLowerCase(), { recommendedTime: value });
}
});
@@ -147,8 +151,16 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
}
private onModelAdded(model: ITextModel): void {
const uri = model.uri;
const supportedSchemes = [Schemas.untitled, Schemas.file, Schemas.vscodeRemote];
if (!uri || !supportedSchemes.includes(uri.scheme)) {
return;
}
this.promptRecommendationsForModel(model);
this._register(model.onDidChangeLanguage(() => this.promptRecommendationsForModel(model)));
const disposables = new DisposableStore();
disposables.add(model.onDidChangeLanguage(() => this.promptRecommendationsForModel(model)));
disposables.add(model.onWillDispose(() => disposables.dispose()));
}
/**
@@ -157,13 +169,8 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
*/
private promptRecommendationsForModel(model: ITextModel): void {
const uri = model.uri;
const supportedSchemes = [Schemas.untitled, Schemas.file, Schemas.vscodeRemote];
if (!uri || !supportedSchemes.includes(uri.scheme)) {
return;
}
const language = model.getLanguageIdentifier().language;
const fileExtension = extname(uri);
const fileExtension = extname(uri).toLowerCase();
if (this.processedLanguages.includes(language) && this.processedFileExtensions.includes(fileExtension)) {
return;
}
@@ -201,21 +208,18 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
for (const recommendation of fileBasedRecommendations) {
const filedBasedRecommendation = this.fileBasedRecommendations.get(recommendation) || { recommendedTime: Date.now(), sources: [] };
filedBasedRecommendation.recommendedTime = Date.now();
if (!filedBasedRecommendation.sources.some(s => s instanceof URI && s.toString() === uri.toString())) {
filedBasedRecommendation.sources.push(uri);
}
this.fileBasedRecommendations.set(recommendation, filedBasedRecommendation);
}
this.storeCachedRecommendations();
if (this.promptedExtensionRecommendations.hasToIgnoreRecommendationNotifications()) {
if (this.extensionRecommendationNotificationService.hasToIgnoreRecommendationNotifications()) {
return;
}
const installed = await this.extensionsWorkbenchService.queryLocal();
if (importantRecommendations.length &&
await this.promptRecommendedExtensionForFileType(languageName || basename(uri), importantRecommendations, installed)) {
await this.promptRecommendedExtensionForFileType(languageName || basename(uri), language, importantRecommendations, installed)) {
return;
}
@@ -232,9 +236,9 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
this.promptRecommendedExtensionForFileExtension(fileExtension, installed);
}
private async promptRecommendedExtensionForFileType(name: string, recommendations: string[], installed: IExtension[]): Promise<boolean> {
private async promptRecommendedExtensionForFileType(name: string, language: string, recommendations: string[], installed: IExtension[]): Promise<boolean> {
recommendations = this.promptedExtensionRecommendations.filterIgnoredOrNotAllowed(recommendations);
recommendations = this.filterIgnoredOrNotAllowed(recommendations);
if (recommendations.length === 0) {
return false;
}
@@ -250,70 +254,101 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
return false;
}
this.promptedExtensionRecommendations.promptImportantExtensionsInstallNotification([extensionId], localize('reallyRecommended', "Do you want to install the recommended extensions for {0}?", name), `@id:${extensionId}`);
const promptedRecommendations = this.getPromptedRecommendations();
if (promptedRecommendations[language] && promptedRecommendations[language].includes(extensionId)) {
return false;
}
this.extensionRecommendationNotificationService.promptImportantExtensionsInstallNotification([extensionId], localize('reallyRecommended', "Do you want to install the recommended extensions for {0}?", name), `@id:${extensionId}`, RecommendationSource.FILE)
.then(result => {
if (result === RecommendationsNotificationResult.Accepted) {
this.addToPromptedRecommendations(language, [extensionId]);
}
});
return true;
}
private getPromptedRecommendations(): IStringDictionary<string[]> {
return JSON.parse(this.storageService.get(promptedRecommendationsStorageKey, StorageScope.GLOBAL, '{}'));
}
private addToPromptedRecommendations(exeName: string, extensions: string[]) {
const promptedRecommendations = this.getPromptedRecommendations();
promptedRecommendations[exeName] = extensions;
this.storageService.store(promptedRecommendationsStorageKey, JSON.stringify(promptedRecommendations), StorageScope.GLOBAL, StorageTarget.USER);
}
private getPromptedFileExtensions(): string[] {
return JSON.parse(this.storageService.get(promptedFileExtensionsStorageKey, StorageScope.GLOBAL, '[]'));
}
private addToPromptedFileExtensions(fileExtension: string) {
const promptedFileExtensions = this.getPromptedFileExtensions();
promptedFileExtensions.push(fileExtension);
this.storageService.store(promptedFileExtensionsStorageKey, JSON.stringify(distinct(promptedFileExtensions)), StorageScope.GLOBAL, StorageTarget.USER);
}
private async promptRecommendedExtensionForFileExtension(fileExtension: string, installed: IExtension[]): Promise<void> {
// {{SQL CARBON EDIT}} - Turn off file extension-based extensions until we have a scenario that requires this. The queryGallery isn't working correctly in ADS now.
let enableRecommendation: boolean = false;
if (!enableRecommendation) {
return;
}
const fileExtensionSuggestionIgnoreList = <string[]>JSON.parse(this.storageService.get('extensionsAssistant/fileExtensionsSuggestionIgnore', StorageScope.GLOBAL, '[]'));
if (fileExtensionSuggestionIgnoreList.indexOf(fileExtension) > -1) {
return;
} else {
const fileExtensionSuggestionIgnoreList = <string[]>JSON.parse(this.storageService.get('extensionsAssistant/fileExtensionsSuggestionIgnore', StorageScope.GLOBAL, '[]'));
if (fileExtensionSuggestionIgnoreList.indexOf(fileExtension) > -1) {
return;
}
const text = `ext:${fileExtension}`;
const pager = await this.extensionsWorkbenchService.queryGallery({ text, pageSize: 100 }, CancellationToken.None);
if (pager.firstPage.length === 0) {
return;
}
const installedExtensionsIds = installed.reduce((result, i) => { result.add(i.identifier.id.toLowerCase()); return result; }, new Set<string>());
if (pager.firstPage.some(e => installedExtensionsIds.has(e.identifier.id.toLowerCase()))) {
return;
}
this.notificationService.prompt(
Severity.Info,
localize('showLanguageExtensions', "The Marketplace has extensions that can help with '.{0}' files", fileExtension),
[{
label: searchMarketplace,
run: () => {
this.telemetryService.publicLog2<{ userReaction: string, fileExtension: string }, FileExtensionSuggestionClassification>('fileExtensionSuggestion:popup', { userReaction: 'ok', fileExtension });
this.viewletService.openViewlet('workbench.view.extensions', true)
.then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer)
.then(viewlet => {
viewlet.search(`ext:${fileExtension}`);
viewlet.focus();
});
}
}, {
label: localize('dontShowAgainExtension', "Don't Show Again for '.{0}' files", fileExtension),
run: () => {
fileExtensionSuggestionIgnoreList.push(fileExtension);
this.storageService.store(
'extensionsAssistant/fileExtensionsSuggestionIgnore',
JSON.stringify(fileExtensionSuggestionIgnoreList),
StorageScope.GLOBAL
);
this.telemetryService.publicLog2<{ userReaction: string, fileExtension: string }, FileExtensionSuggestionClassification>('fileExtensionSuggestion:popup', { userReaction: 'neverShowAgain', fileExtension });
}
}],
{
sticky: true,
onCancel: () => {
this.telemetryService.publicLog2<{ userReaction: string, fileExtension: string }, FileExtensionSuggestionClassification>('fileExtensionSuggestion:popup', { userReaction: 'cancelled', fileExtension });
}
}
);
}
const promptedFileExtensions = this.getPromptedFileExtensions();
if (promptedFileExtensions.includes(fileExtension)) {
return;
}
const text = `ext:${fileExtension}`;
const pager = await this.extensionsWorkbenchService.queryGallery({ text, pageSize: 100 }, CancellationToken.None);
if (pager.firstPage.length === 0) {
return;
}
const installedExtensionsIds = installed.reduce((result, i) => { result.add(i.identifier.id.toLowerCase()); return result; }, new Set<string>());
if (pager.firstPage.some(e => installedExtensionsIds.has(e.identifier.id.toLowerCase()))) {
return;
}
this.notificationService.prompt(
Severity.Info,
localize('showLanguageExtensions', "The Marketplace has extensions that can help with '.{0}' files", fileExtension),
[{
label: searchMarketplace,
run: () => {
this.addToPromptedFileExtensions(fileExtension);
this.telemetryService.publicLog2<{ userReaction: string, fileExtension: string }, FileExtensionSuggestionClassification>('fileExtensionSuggestion:popup', { userReaction: 'ok', fileExtension });
this.viewletService.openViewlet('workbench.view.extensions', true)
.then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer)
.then(viewlet => {
viewlet.search(`ext:${fileExtension}`);
viewlet.focus();
});
}
}, {
label: localize('dontShowAgainExtension', "Don't Show Again for '.{0}' files", fileExtension),
run: () => {
fileExtensionSuggestionIgnoreList.push(fileExtension);
this.storageService.store(
'extensionsAssistant/fileExtensionsSuggestionIgnore',
JSON.stringify(fileExtensionSuggestionIgnoreList),
StorageScope.GLOBAL,
StorageTarget.USER);
this.telemetryService.publicLog2<{ userReaction: string, fileExtension: string }, FileExtensionSuggestionClassification>('fileExtensionSuggestion:popup', { userReaction: 'neverShowAgain', fileExtension });
}
}],
{
sticky: true,
onCancel: () => {
this.telemetryService.publicLog2<{ userReaction: string, fileExtension: string }, FileExtensionSuggestionClassification>('fileExtensionSuggestion:popup', { userReaction: 'cancelled', fileExtension });
}
}
);
}
private filterIgnoredOrNotAllowed(recommendationsToSuggest: string[]): string[] {
const ignoredRecommendations = [...this.extensionIgnoredRecommendationsService.ignoredRecommendations, ...this.extensionRecommendationNotificationService.ignoredRecommendations];
return recommendationsToSuggest.filter(id => !ignoredRecommendations.includes(id));
}
private filterInstalled(recommendationsToSuggest: string[], installed: IExtension[]): string[] {
@@ -343,6 +378,6 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
private storeCachedRecommendations(): void {
const storedRecommendations: IStringDictionary<number> = {};
this.fileBasedRecommendations.forEach((value, key) => storedRecommendations[key] = value.recommendedTime);
this.storageService.store(recommendationsStorageKey, JSON.stringify(storedRecommendations), StorageScope.GLOBAL);
this.storageService.store(recommendationsStorageKey, JSON.stringify(storedRecommendations), StorageScope.GLOBAL, StorageTarget.MACHINE);
}
}

View File

@@ -3,9 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { IProductService } from 'vs/platform/product/common/productService';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ExtensionRecommendationReason, IExtensionRecommendationReson } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
export class KeymapRecommendations extends ExtensionRecommendations {
@@ -13,18 +13,16 @@ export class KeymapRecommendations extends ExtensionRecommendations {
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
constructor(
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IProductService private readonly productService: IProductService,
) {
super(promptedExtensionRecommendations);
super();
}
protected async doActivate(): Promise<void> {
if (this.productService.keymapExtensionTips) {
this._recommendations = this.productService.keymapExtensionTips.map(extensionId => (<ExtensionRecommendation>{
extensionId: extensionId.toLowerCase(),
source: 'application',
reason: {
reason: <IExtensionRecommendationReson>{ // {{SQL CARBON EDIT}}
reasonId: ExtensionRecommendationReason.Application,
reasonText: ''
}

View File

@@ -67,7 +67,7 @@
.extension-list-item > .details > .header-container > .header {
display: flex;
align-items: baseline;
align-items: center;
flex-wrap: nowrap;
overflow: hidden;
flex: 1;
@@ -139,6 +139,15 @@
color: currentColor;
}
.extension-list-item > .details > .header-container > .header .sync-ignored {
display: flex;
margin-left: 6px;
}
.extension-list-item > .details > .header-container > .header .sync-ignored > .codicon {
font-size: 100%;
}
.extension-list-item > .details > .description {
padding-right: 11px;
}

View File

@@ -10,26 +10,29 @@
text-overflow: ellipsis;
}
.monaco-action-bar .action-item .action-label.extension-action.label {
.monaco-action-bar .action-item > .action-label.extension-action.label,
.monaco-action-bar .action-dropdown-item > .action-label.extension-action.label {
padding: 0 5px;
outline-offset: 2px;
}
.monaco-action-bar .action-item .action-label.extension-action.label {
outline-offset: 1px;
}
.monaco-action-bar .action-item .action-label.extension-action.text,
.monaco-action-bar .action-item .action-label.extension-action.label {
.monaco-action-bar .action-item .action-label.extension-action.label,
.monaco-action-bar .action-dropdown-item .action-label.extension-action.label {
line-height: 14px;
margin-top: 2px;
}
.monaco-action-bar .action-item .action-label.extension-action.icon {
padding: 0 2px;
height: 18px;
width: 16px;
}
.monaco-action-bar .action-item .action-label.extension-action.multiserver.install:after,
.monaco-action-bar .action-item .action-label.extension-action.multiserver.update:after,
.monaco-action-bar .action-item .action-label.extension-action.extension-editor-dropdown-action.dropdown:after {
.monaco-action-bar .action-item .action-label.extension-action.multiserver.update:after {
content: '▼';
padding-left: 2px;
font-size: 80%;
@@ -42,7 +45,9 @@
.monaco-action-bar .action-item.disabled .action-label.extension-action.uninstall:not(.uninstalling),
.monaco-action-bar .action-item.disabled .action-label.extension-action.update,
.monaco-action-bar .action-item.disabled .action-label.extension-action.theme,
.monaco-action-bar .action-item.disabled .action-label.extension-action.extension-editor-dropdown-action,
.monaco-action-bar .action-item.disabled .action-label.extension-action.extension-sync,
.monaco-action-bar .action-item.action-dropdown-item.disabled,
.monaco-action-bar .action-item.action-dropdown-item .action-label.extension-action.hide,
.monaco-action-bar .action-item.disabled .action-label.extension-action.reload,
.monaco-action-bar .action-item.disabled .action-label.disable-status.hide,
.monaco-action-bar .action-item.disabled .action-label.system-disable.hide,

View File

@@ -76,7 +76,6 @@
.extension-editor > .header > .details > .title > .identifier {
margin-left: 10px;
font-size: 14px;
opacity: 0.6;
background: rgba(173, 173, 173, 0.31);
padding: 0px 4px;
border-radius: 4px;
@@ -153,16 +152,30 @@
justify-content: flex-start;
}
.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .extension-action {
.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item:not(.action-dropdown-item) > .extension-action,
.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item.action-dropdown-item .extension-action.action-label.dropdown,
.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item.action-dropdown-item > .extension-action.action-label.empty-dropdown {
margin-right: 8px;
}
.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .extension-action.label {
.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .extension-action.label,
.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item.action-dropdown-item .extension-action.label {
font-weight: 600;
padding: 1px 6px;
max-width: 300px;
}
.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .extension-action.icon {
padding-top: 1px;
}
.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .extension-action.label {
padding: 1px 6px;
}
.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item.action-dropdown-item .extension-action.dropdown.label {
padding: 1px 0px;
}
.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .action-label.system-disable {
margin-right: 0;
}
@@ -202,8 +215,7 @@
.extension-editor > .header > .details > .subtext-container > .monaco-action-bar .action-label {
margin-top: 4px;
margin-left: 4px;
padding-top: 0;
padding-bottom: 2px;
padding-bottom: 1px;
}
.extension-editor > .header.recommended > .details > .recommendation > .monaco-action-bar .actions-container {
@@ -387,7 +399,7 @@
word-break: keep-all;
}
.extension-editor > .body > .content table code:not(:empty) {
.extension-editor > .body > .content code:not(:empty) {
font-family: var(--monaco-monospace-font);
font-size: 90%;
background-color: rgba(128, 128, 128, 0.17);

View File

@@ -52,6 +52,12 @@
display: none;
}
/* {{SQL CARBON EDIT}} - hide drop-down to avoid rendering bug
.extension-editor .details .monaco-action-bar .monaco-dropdown,
.extensions-viewlet > .extensions .monaco-action-bar .monaco-dropdown {
width: 0px;
}
.extensions-viewlet > .extensions .extensions-list.hidden,
.extensions-viewlet > .extensions .message-container.hidden {
display: none;

View File

@@ -3,6 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.extension-sync-ignored.hide {
display: none;
}
.extension-ratings {
display: inline-block;
}
@@ -11,7 +15,7 @@
font-size: 80%;
}
.extension-ratings > .codicon[class*='codicon-star']:not(:first-child) {
.extension-ratings > .codicon[class*='codicon-extensions-rating']:not(:first-child) {
margin-left: 3px;
}
@@ -24,12 +28,17 @@
}
/* TODO @misolori make this a color token */
.extension-ratings .codicon-star-full,
.extension-ratings .codicon-star-half {
.extension-ratings .codicon-extensions-star-full,
.extension-ratings .codicon-extensions-star-half {
color: #FF8E00 !important;
}
.extension-ratings .codicon-star-empty {
.extension-install-count .codicon,
.extension-action.codicon-extensions-manage {
color: inherit;
}
.extension-ratings .codicon-extensions-star-empty {
opacity: .4;
}

View File

@@ -57,11 +57,6 @@
color: currentColor;
}
.monaco-workbench.vs .runtime-extensions-editor .extension > .icon-container > .icon,
.monaco-workbench.vs-dark .runtime-extensions-editor .extension > .icon-container > .icon {
opacity: 0.5;
}
.runtime-extensions-editor .extension > .desc > .header-container {
display: flex;
overflow: hidden;

View File

@@ -5,13 +5,14 @@
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { MenuRegistry, MenuId, Action2, registerAction2 } from 'vs/platform/actions/common/actions';
import { localize } from 'vs/nls';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ILabelService } from 'vs/platform/label/common/label';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { InstallLocalExtensionsInRemoteAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { InstallLocalExtensionsInRemoteAction, InstallRemoteExtensionsInLocalAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
export class RemoteExtensionsInstaller extends Disposable implements IWorkbenchContribution {
@@ -38,6 +39,20 @@ export class RemoteExtensionsInstaller extends Disposable implements IWorkbenchC
appendMenuItem();
this._register(labelService.onDidChangeFormatters(e => appendMenuItem()));
this._register(toDisposable(() => disposable.dispose()));
this._register(registerAction2(class InstallRemoteExtensionsInLocalAction2 extends Action2 {
constructor() {
super({
id: 'workbench.extensions.actions.installLocalExtensionsInRemote',
title: { value: localize('install remote in local', "Install Remote Extensions Locally..."), original: 'Install Remote Extensions Locally...' },
category: localize({ key: 'remote', comment: ['Remote as in remote machine'] }, "Remote"),
f1: true
});
}
run(accessor: ServicesAccessor): Promise<void> {
return accessor.get(IInstantiationService).createInstance(InstallRemoteExtensionsInLocalAction, 'workbench.extensions.actions.installLocalExtensionsInRemote').run();
}
}));
}
}

View File

@@ -4,18 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import { EXTENSION_IDENTIFIER_PATTERN, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IWorkspaceContextService, IWorkspaceFolder, IWorkspace, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
import { IFileService } from 'vs/platform/files/common/files';
import { distinct, flatten, coalesce } from 'vs/base/common/arrays';
import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { distinct, flatten } from 'vs/base/common/arrays';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IExtensionsConfigContent, ExtensionRecommendationSource, ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { parse } from 'vs/base/common/json';
import { EXTENSIONS_CONFIG } from 'vs/workbench/contrib/extensions/common/extensions';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { ILogService } from 'vs/platform/log/common/log';
import { CancellationToken } from 'vs/base/common/cancellation';
import { localize } from 'vs/nls';
import { Emitter } from 'vs/base/common/event';
import { IExtensionsConfigContent, IWorkpsaceExtensionsConfigService } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig';
export class WorkspaceRecommendations extends ExtensionRecommendations {
@@ -29,19 +26,17 @@ export class WorkspaceRecommendations extends ExtensionRecommendations {
get ignoredRecommendations(): ReadonlyArray<string> { return this._ignoredRecommendations; }
constructor(
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IWorkpsaceExtensionsConfigService private readonly workpsaceExtensionsConfigService: IWorkpsaceExtensionsConfigService,
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
@ILogService private readonly logService: ILogService,
@IFileService private readonly fileService: IFileService,
@INotificationService private readonly notificationService: INotificationService,
) {
super(promptedExtensionRecommendations);
super();
}
protected async doActivate(): Promise<void> {
await this.fetch();
this._register(this.contextService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e)));
this._register(this.workpsaceExtensionsConfigService.onDidChangeExtensionsConfigs(() => this.onDidChangeExtensionsConfigs()));
}
/**
@@ -49,71 +44,40 @@ export class WorkspaceRecommendations extends ExtensionRecommendations {
*/
private async fetch(): Promise<void> {
const extensionsConfigBySource = await this.fetchExtensionsConfigBySource();
const extensionsConfigs = await this.workpsaceExtensionsConfigService.getExtensionsConfigs();
const { invalidRecommendations, message } = await this.validateExtensions(extensionsConfigBySource.map(({ contents }) => contents));
const { invalidRecommendations, message } = await this.validateExtensions(extensionsConfigs);
if (invalidRecommendations.length) {
this.notificationService.warn(`The ${invalidRecommendations.length} extension(s) below, in workspace recommendations have issues:\n${message}`);
}
this._recommendations = [];
this._ignoredRecommendations = [];
for (const extensionsConfig of extensionsConfigBySource) {
for (const unwantedRecommendation of extensionsConfig.contents.unwantedRecommendations) {
if (invalidRecommendations.indexOf(unwantedRecommendation) === -1) {
this._ignoredRecommendations.push(unwantedRecommendation);
for (const extensionsConfig of extensionsConfigs) {
if (extensionsConfig.unwantedRecommendations) {
for (const unwantedRecommendation of extensionsConfig.unwantedRecommendations) {
if (invalidRecommendations.indexOf(unwantedRecommendation) === -1) {
this._ignoredRecommendations.push(unwantedRecommendation);
}
}
}
for (const extensionId of extensionsConfig.contents.recommendations) {
if (invalidRecommendations.indexOf(extensionId) === -1) {
this._recommendations.push({
extensionId,
source: extensionsConfig.source,
reason: {
reasonId: ExtensionRecommendationReason.Workspace,
reasonText: localize('workspaceRecommendation', "This extension is recommended by users of the current workspace.")
}
});
if (extensionsConfig.recommendations) {
for (const extensionId of extensionsConfig.recommendations) {
if (invalidRecommendations.indexOf(extensionId) === -1) {
this._recommendations.push({
extensionId,
reason: {
reasonId: ExtensionRecommendationReason.Workspace,
reasonText: localize('workspaceRecommendation', "This extension is recommended by users of the current workspace.")
}
});
}
}
}
}
}
private async fetchExtensionsConfigBySource(): Promise<{ contents: IExtensionsConfigContent, source: ExtensionRecommendationSource }[]> {
const workspace = this.contextService.getWorkspace();
const result = await Promise.all([
this.resolveWorkspaceExtensionConfig(workspace),
...workspace.folders.map(workspaceFolder => this.resolveWorkspaceFolderExtensionConfig(workspaceFolder))
]);
return coalesce(result);
}
private async resolveWorkspaceExtensionConfig(workspace: IWorkspace): Promise<{ contents: IExtensionsConfigContent, source: ExtensionRecommendationSource } | null> {
try {
if (workspace.configuration) {
const content = await this.fileService.readFile(workspace.configuration);
const extensionsConfigContent = <IExtensionsConfigContent | undefined>parse(content.value.toString())['extensions'];
const contents = this.parseExtensionConfig(extensionsConfigContent);
if (contents) {
return { contents, source: workspace };
}
}
} catch (e) { /* Ignore */ }
return null;
}
private async resolveWorkspaceFolderExtensionConfig(workspaceFolder: IWorkspaceFolder): Promise<{ contents: IExtensionsConfigContent, source: ExtensionRecommendationSource } | null> {
try {
const content = await this.fileService.readFile(workspaceFolder.toResource(EXTENSIONS_CONFIG));
const extensionsConfigContent = <IExtensionsConfigContent>parse(content.value.toString());
const contents = this.parseExtensionConfig(extensionsConfigContent);
if (contents) {
return { contents, source: workspaceFolder };
}
} catch (e) { /* ignore */ }
return null;
}
private async validateExtensions(contents: IExtensionsConfigContent[]): Promise<{ validRecommendations: string[], invalidRecommendations: string[], message: string }> {
const validExtensions: string[] = [];
@@ -154,25 +118,9 @@ export class WorkspaceRecommendations extends ExtensionRecommendations {
return { validRecommendations: validExtensions, invalidRecommendations: invalidExtensions, message };
}
private async onWorkspaceFoldersChanged(event: IWorkspaceFoldersChangeEvent): Promise<void> {
if (event.added.length) {
const oldWorkspaceRecommended = this._recommendations;
await this.fetch();
// Suggest only if at least one of the newly added recommendations was not suggested before
if (this._recommendations.some(current => oldWorkspaceRecommended.every(old => current.extensionId !== old.extensionId))) {
this._onDidChangeRecommendations.fire();
}
}
}
private parseExtensionConfig(extensionsConfigContent: IExtensionsConfigContent | undefined): IExtensionsConfigContent | null {
if (extensionsConfigContent) {
return {
recommendations: distinct((extensionsConfigContent.recommendations || []).map(e => e.toLowerCase())),
unwantedRecommendations: distinct((extensionsConfigContent.unwantedRecommendations || []).map(e => e.toLowerCase()))
};
}
return null;
private async onDidChangeExtensionsConfigs(): Promise<void> {
await this.fetch();
this._onDidChangeRecommendations.fire();
}
}

View File

@@ -6,7 +6,7 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { IPager } from 'vs/base/common/paging';
import { IQueryOptions, ILocalExtension, IGalleryExtension, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IQueryOptions, ILocalExtension, IGalleryExtension, IExtensionIdentifier, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { EnablementState, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Disposable } from 'vs/base/common/lifecycle';
@@ -20,7 +20,8 @@ export const VIEWLET_ID = 'workbench.view.extensions';
export const EXTENSIONS_CONFIG = '.azuredatastudio/extensions.json';
export interface IExtensionsViewPaneContainer extends IViewPaneContainer {
search(text: string, refresh?: boolean): void;
search(text: string): void;
refresh(): Promise<void>;
}
export const enum ExtensionState {
@@ -32,6 +33,7 @@ export const enum ExtensionState {
export interface IExtension {
readonly type: ExtensionType;
readonly isBuiltin: boolean;
readonly state: ExtensionState;
readonly name: string;
readonly displayName: string;
@@ -82,7 +84,7 @@ export interface IExtensionsWorkbenchService {
queryGallery(options: IQueryOptions, token: CancellationToken): Promise<IPager<IExtension>>;
canInstall(extension: IExtension): boolean;
install(vsix: URI): Promise<IExtension>;
install(extension: IExtension, promptToInstallDependencies?: boolean): Promise<IExtension>;
install(extension: IExtension, installOptins?: InstallOptions): Promise<IExtension>;
uninstall(extension: IExtension): Promise<void>;
installVersion(extension: IExtension, version: string): Promise<IExtension>;
reinstall(extension: IExtension): Promise<IExtension>;
@@ -135,10 +137,12 @@ export class ExtensionContainers extends Disposable {
for (const container of this.containers) {
if (extension && container.extension) {
if (areSameExtensions(container.extension.identifier, extension.identifier)) {
if (!container.extension.server || !extension.server || container.extension.server === extension.server) {
if (container.extension.server && extension.server && container.extension.server !== extension.server) {
if (container.updateWhenCounterExtensionChanges) {
container.update();
}
} else {
container.extension = extension;
} else if (container.updateWhenCounterExtensionChanges) {
container.update();
}
}
} else {
@@ -149,3 +153,4 @@ export class ExtensionContainers extends Disposable {
}
export const TOGGLE_IGNORE_EXTENSION_ACTION_ID = 'workbench.extensions.action.toggleIgnoreExtension';
export const INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID = 'workbench.extensions.command.installFromVSIX';

View File

@@ -9,6 +9,7 @@ import { localize } from 'vs/nls';
import { EditorInput } from 'vs/workbench/common/editor';
import { IExtension } from 'vs/workbench/contrib/extensions/common/extensions';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { join } from 'vs/base/common/path';
export class ExtensionsInput extends EditorInput {
@@ -17,7 +18,7 @@ export class ExtensionsInput extends EditorInput {
get resource() {
return URI.from({
scheme: Schemas.extension,
path: this.extension.identifier.id
path: join(this.extension.identifier.id, 'extension')
});
}

View File

@@ -9,8 +9,9 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { Disposable } from 'vs/base/common/lifecycle';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IExtensionManagementService, ILocalExtension, IExtensionIdentifier, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionRecommendationsService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { IWorkbenchExtensionEnablementService, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
@@ -123,5 +124,5 @@ export async function getInstalledExtensions(accessor: ServicesAccessor): Promis
export function isKeymapExtension(tipsService: IExtensionRecommendationsService, extension: IExtensionStatus): boolean {
const cats = extension.local.manifest.categories;
return cats && cats.indexOf('Keymaps') !== -1 || tipsService.getKeymapRecommendations().some(({ extensionId }) => areSameExtensions({ id: extensionId }, extension.local.identifier));
return cats && cats.indexOf('Keymaps') !== -1 || tipsService.getKeymapRecommendations().some(extensionId => areSameExtensions({ id: extensionId }, extension.local.identifier));
}

View File

@@ -0,0 +1,55 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { IProductService } from 'vs/platform/product/common/productService';
import { Action } from 'vs/base/common/actions';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
import { IDebugService } from 'vs/workbench/contrib/debug/common/debug';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { randomPort } from 'vs/base/node/ports';
export class DebugExtensionHostAction extends Action {
static readonly ID = 'workbench.extensions.action.debugExtensionHost';
static readonly LABEL = nls.localize('debugExtensionHost', "Start Debugging Extension Host");
static readonly CSS_CLASS = 'debug-extension-host';
constructor(
@IDebugService private readonly _debugService: IDebugService,
@INativeHostService private readonly _nativeHostService: INativeHostService,
@IDialogService private readonly _dialogService: IDialogService,
@IExtensionService private readonly _extensionService: IExtensionService,
@IProductService private readonly productService: IProductService
) {
super(DebugExtensionHostAction.ID, DebugExtensionHostAction.LABEL, DebugExtensionHostAction.CSS_CLASS);
}
async run(): Promise<any> {
const inspectPort = await this._extensionService.getInspectPort(false);
if (!inspectPort) {
const res = await this._dialogService.confirm({
type: 'info',
message: nls.localize('restart1', "Profile Extensions"),
detail: nls.localize('restart2', "In order to profile extensions a restart is required. Do you want to restart '{0}' now?", this.productService.nameLong),
primaryButton: nls.localize('restart3', "&&Restart"),
secondaryButton: nls.localize('cancel', "&&Cancel")
});
if (res.confirmed) {
await this._nativeHostService.relaunch({ addArgs: [`--inspect-extensions=${randomPort()}`] });
}
return;
}
return this._debugService.startDebugging(undefined, {
type: 'node',
name: nls.localize('debugExtensionHost.launch.name', "Attach Extension Host"),
request: 'attach',
port: inspectPort
});
}
}

View File

@@ -12,11 +12,11 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar';
import { IExtensionHostProfileService, ProfileSessionState } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { randomPort } from 'vs/base/node/ports';
import { IProductService } from 'vs/platform/product/common/productService';
import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput';
import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { ExtensionHostProfiler } from 'vs/workbench/services/extensions/electron-browser/extensionHostProfiler';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
@@ -46,7 +46,7 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio
@IExtensionService private readonly _extensionService: IExtensionService,
@IEditorService private readonly _editorService: IEditorService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IElectronService private readonly _electronService: IElectronService,
@INativeHostService private readonly _nativeHostService: INativeHostService,
@IDialogService private readonly _dialogService: IDialogService,
@IStatusbarService private readonly _statusbarService: IStatusbarService,
@IProductService private readonly _productService: IProductService
@@ -56,9 +56,9 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio
this._profileSession = null;
this._setState(ProfileSessionState.None);
CommandsRegistry.registerCommand('workbench.action.extensionHostProfilder.stop', () => {
CommandsRegistry.registerCommand('workbench.action.extensionHostProfiler.stop', () => {
this.stopProfiling();
this._editorService.openEditor(RuntimeExtensionsInput.instance, { revealIfOpened: true });
this._editorService.openEditor(RuntimeExtensionsInput.instance, { revealIfOpened: true, pinned: true });
});
}
@@ -82,16 +82,17 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio
if (visible) {
const indicator: IStatusbarEntry = {
text: '$(sync~spin) ' + nls.localize('profilingExtensionHost', "Profiling Extension Host"),
text: nls.localize('profilingExtensionHost', "Profiling Extension Host"),
showProgress: true,
ariaLabel: nls.localize('profilingExtensionHost', "Profiling Extension Host"),
tooltip: nls.localize('selectAndStartDebug', "Click to stop profiling."),
command: 'workbench.action.extensionHostProfilder.stop'
command: 'workbench.action.extensionHostProfiler.stop'
};
const timeStarted = Date.now();
const handle = setInterval(() => {
if (this.profilingStatusBarIndicator) {
this.profilingStatusBarIndicator.update({ ...indicator, text: '$(sync~spin) ' + nls.localize('profilingExtensionHostTime', "Profiling Extension Host ({0} sec)", Math.round((new Date().getTime() - timeStarted) / 1000)), });
this.profilingStatusBarIndicator.update({ ...indicator, text: nls.localize('profilingExtensionHostTime', "Profiling Extension Host ({0} sec)", Math.round((new Date().getTime() - timeStarted) / 1000)), });
}
}, 1000);
this.profilingStatusBarIndicatorLabelUpdater.value = toDisposable(() => clearInterval(handle));
@@ -120,11 +121,11 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio
type: 'info',
message: nls.localize('restart1', "Profile Extensions"),
detail: nls.localize('restart2', "In order to profile extensions a restart is required. Do you want to restart '{0}' now?", this._productService.nameLong),
primaryButton: nls.localize('restart3', "Restart"),
secondaryButton: nls.localize('cancel', "Cancel")
primaryButton: nls.localize('restart3', "&&Restart"),
secondaryButton: nls.localize('cancel', "&&Cancel")
}).then(res => {
if (res.confirmed) {
this._electronService.relaunch({ addArgs: [`--inspect-extensions=${randomPort()}`] });
this._nativeHostService.relaunch({ addArgs: [`--inspect-extensions=${randomPort()}`] });
}
});
}

View File

@@ -13,38 +13,34 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { EditorDescriptor, IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { RuntimeExtensionsEditor, ShowRuntimeExtensionsAction, IExtensionHostProfileService, DebugExtensionHostAction, StartExtensionHostProfileAction, StopExtensionHostProfileAction, CONTEXT_PROFILE_SESSION_STATE, SaveExtensionHostProfileAction, CONTEXT_EXTENSION_HOST_PROFILE_RECORDED } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { RuntimeExtensionsEditor, IExtensionHostProfileService, StartExtensionHostProfileAction, StopExtensionHostProfileAction, CONTEXT_PROFILE_SESSION_STATE, CONTEXT_EXTENSION_HOST_PROFILE_RECORDED, SaveExtensionHostProfileAction } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor';
import { DebugExtensionHostAction } from 'vs/workbench/contrib/extensions/electron-browser/debugExtensionHostAction';
import { EditorInput, IEditorInputFactory, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, ActiveEditorContext } from 'vs/workbench/common/editor';
import { ExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-browser/extensionProfileService';
import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput';
import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { ExtensionsAutoProfiler } from 'vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService';
import { OpenExtensionsFolderAction } from 'vs/workbench/contrib/extensions/electron-sandbox/extensionsActions';
import { ExtensionsLabel } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService';
import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
import { ExtensionRecommendationNotificationServiceChannel } from 'vs/platform/extensionRecommendations/electron-sandbox/extensionRecommendationsIpc';
import { Codicon } from 'vs/base/common/codicons';
// Singletons
registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); // TODO@sandbox TODO@ben move back into common/extensions.contribution.ts when 'semver-umd' can be loaded
registerSingleton(IExtensionHostProfileService, ExtensionHostProfileService, true);
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchRegistry.registerWorkbenchContribution(ExtensionsAutoProfiler, LifecyclePhase.Eventually);
// Running Extensions Editor
const runtimeExtensionsEditorDescriptor = EditorDescriptor.create(
RuntimeExtensionsEditor,
RuntimeExtensionsEditor.ID,
localize('runtimeExtension', "Running Extensions")
Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
EditorDescriptor.create(RuntimeExtensionsEditor, RuntimeExtensionsEditor.ID, localize('runtimeExtension', "Running Extensions")),
[new SyncDescriptor(RuntimeExtensionsInput)]
);
Registry.as<IEditorRegistry>(EditorExtensions.Editors)
.registerEditor(runtimeExtensionsEditorDescriptor, [new SyncDescriptor(RuntimeExtensionsInput)]);
class RuntimeExtensionsInputFactory implements IEditorInputFactory {
canSerialize(editorInput: EditorInput): boolean {
return true;
@@ -63,17 +59,16 @@ Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactor
// Global actions
const actionRegistry = Registry.as<IWorkbenchActionRegistry>(WorkbenchActionExtensions.WorkbenchActions);
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ShowRuntimeExtensionsAction), 'Show Running Extensions', localize({ key: 'developer', comment: ['A developer on Code itself or someone diagnosing issues in Code'] }, "Developer"));
class ExtensionsContributions implements IWorkbenchContribution {
constructor(
@IWorkbenchEnvironmentService workbenchEnvironmentService: INativeWorkbenchEnvironmentService
@INativeWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService,
@IExtensionRecommendationNotificationService extensionRecommendationNotificationService: IExtensionRecommendationNotificationService,
@ISharedProcessService sharedProcessService: ISharedProcessService,
) {
if (workbenchEnvironmentService.extensionsPath) {
const openExtensionsFolderActionDescriptor = SyncActionDescriptor.from(OpenExtensionsFolderAction);
actionRegistry.registerWorkbenchAction(openExtensionsFolderActionDescriptor, 'Extensions: Open Extensions Folder', ExtensionsLabel);
}
sharedProcessService.registerChannel('IExtensionRecommendationNotificationService', new ExtensionRecommendationNotificationServiceChannel(extensionRecommendationNotificationService));
const openExtensionsFolderActionDescriptor = SyncActionDescriptor.from(OpenExtensionsFolderAction);
actionRegistry.registerWorkbenchAction(openExtensionsFolderActionDescriptor, 'Extensions: Open Extensions Folder', ExtensionsLabel);
}
}
@@ -107,9 +102,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
command: {
id: DebugExtensionHostAction.ID,
title: DebugExtensionHostAction.LABEL,
icon: {
id: 'codicon/debug-start'
}
icon: Codicon.debugStart
},
group: 'navigation',
when: ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID)
@@ -119,9 +112,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
command: {
id: StartExtensionHostProfileAction.ID,
title: StartExtensionHostProfileAction.LABEL,
icon: {
id: 'codicon/circle-filled'
}
icon: Codicon.circleFilled
},
group: 'navigation',
when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID), CONTEXT_PROFILE_SESSION_STATE.notEqualsTo('running'))
@@ -131,9 +122,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
command: {
id: StopExtensionHostProfileAction.ID,
title: StopExtensionHostProfileAction.LABEL,
icon: {
id: 'codicon/debug-stop'
}
icon: Codicon.debugStop
},
group: 'navigation',
when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID), CONTEXT_PROFILE_SESSION_STATE.isEqualTo('running'))
@@ -143,9 +132,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
command: {
id: SaveExtensionHostProfileAction.ID,
title: SaveExtensionHostProfileAction.LABEL,
icon: {
id: 'codicon/save-all'
},
icon: Codicon.saveAll,
precondition: CONTEXT_EXTENSION_HOST_PROFILE_RECORDED
},
group: 'navigation',

View File

@@ -10,18 +10,18 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { onUnexpectedError } from 'vs/base/common/errors';
import * as os from 'os';
import { join } from 'vs/base/common/path';
import { joinPath } from 'vs/base/common/resources';
import { writeFile } from 'vs/base/node/pfs';
import { IExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { localize } from 'vs/nls';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput';
import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { createSlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions';
import { ExtensionHostProfiler } from 'vs/workbench/services/extensions/electron-browser/extensionHostProfiler';
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService';
export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchContribution {
@@ -36,6 +36,7 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont
@INotificationService private readonly _notificationService: INotificationService,
@IEditorService private readonly _editorService: IEditorService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@INativeWorkbenchEnvironmentService private readonly _environmentServie: INativeWorkbenchEnvironmentService
) {
super();
this._register(_extensionService.onDidChangeResponsiveChange(this._onDidChangeResponsiveChange, this));
@@ -138,7 +139,7 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont
// print message to log
const path = join(os.tmpdir(), `exthost-${Math.random().toString(16).slice(2, 8)}.cpuprofile`);
const path = joinPath(this._environmentServie.tmpDir, `exthost-${Math.random().toString(16).slice(2, 8)}.cpuprofile`).fsPath;
await writeFile(path, JSON.stringify(profile.data));
this._logService.warn(`UNRESPONSIVE extension host, '${top.id}' took ${top!.percentage}% of ${duration / 1e3}ms, saved PROFILE here: '${path}'`, data);
@@ -186,7 +187,7 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont
),
[{
label: localize('show', 'Show Extensions'),
run: () => this._editorService.openEditor(RuntimeExtensionsInput.instance)
run: () => this._editorService.openEditor(RuntimeExtensionsInput.instance, { pinned: true })
},
action
],

View File

@@ -3,7 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import { IProductService } from 'vs/platform/product/common/productService';
import { Action } from 'vs/base/common/actions';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
@@ -13,11 +12,13 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati
import { localize } from 'vs/nls';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IRequestService, asText } from 'vs/platform/request/common/request';
import { join } from 'vs/base/common/path';
import { joinPath } from 'vs/base/common/resources';
import { onUnexpectedError } from 'vs/base/common/errors';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import Severity from 'vs/base/common/severity';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService';
abstract class RepoInfo {
abstract get base(): string;
@@ -119,7 +120,9 @@ class ReportExtensionSlowAction extends Action {
readonly profile: IExtensionHostProfile,
@IDialogService private readonly _dialogService: IDialogService,
@IOpenerService private readonly _openerService: IOpenerService,
@IProductService private readonly _productService: IProductService
@IProductService private readonly _productService: IProductService,
@INativeHostService private readonly _nativeHostService: INativeHostService,
@INativeWorkbenchEnvironmentService private readonly _environmentService: INativeWorkbenchEnvironmentService
) {
super('report.slow', localize('cmd.report', "Report Issue"));
}
@@ -129,12 +132,13 @@ class ReportExtensionSlowAction extends Action {
// rewrite pii (paths) and store on disk
const profiler = await import('v8-inspect-profiler');
const data = profiler.rewriteAbsolutePaths({ profile: <any>this.profile.data }, 'pii_removed');
const path = join(os.homedir(), `${this.extension.identifier.value}-unresponsive.cpuprofile.txt`);
const path = joinPath(this._environmentService.tmpDir, `${this.extension.identifier.value}-unresponsive.cpuprofile.txt`).fsPath;
await profiler.writeProfile(data, path).then(undefined, onUnexpectedError);
// build issue
const os = await this._nativeHostService.getOSProperties();
const title = encodeURIComponent('Extension causes high cpu load');
const osVersion = `${os.type()} ${os.arch()} ${os.release()}`;
const osVersion = `${os.type} ${os.arch} ${os.release}`;
const message = `:warning: Make sure to **attach** this file from your *home*-directory:\n:warning:\`${path}\`\n\nFind more details here: https://github.com/microsoft/vscode/wiki/Explain-extension-causes-high-cpu-load`;
const body = encodeURIComponent(`- Issue Type: \`Performance\`
- Extension Name: \`${this.extension.name}\`
@@ -161,7 +165,8 @@ class ShowExtensionSlowAction extends Action {
readonly repoInfo: RepoInfo,
readonly profile: IExtensionHostProfile,
@IDialogService private readonly _dialogService: IDialogService,
@IOpenerService private readonly _openerService: IOpenerService
@IOpenerService private readonly _openerService: IOpenerService,
@INativeWorkbenchEnvironmentService private readonly _environmentService: INativeWorkbenchEnvironmentService
) {
super('show.slow', localize('cmd.show', "Show Issues"));
}
@@ -171,7 +176,7 @@ class ShowExtensionSlowAction extends Action {
// rewrite pii (paths) and store on disk
const profiler = await import('v8-inspect-profiler');
const data = profiler.rewriteAbsolutePaths({ profile: <any>this.profile.data }, 'pii_removed');
const path = join(os.homedir(), `${this.extension.identifier.value}-unresponsive.cpuprofile.txt`);
const path = joinPath(this._environmentService.tmpDir, `${this.extension.identifier.value}-unresponsive.cpuprofile.txt`).fsPath;
await profiler.writeProfile(data, path).then(undefined, onUnexpectedError);
// show issues

View File

@@ -0,0 +1,78 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { IProductService } from 'vs/platform/product/common/productService';
import { Action } from 'vs/base/common/actions';
import { IExtension } from 'vs/workbench/contrib/extensions/common/extensions';
import { IExtensionsStatus, IExtensionHostProfile } from 'vs/workbench/services/extensions/common/extensions';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
import { ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { URI } from 'vs/base/common/uri';
export class ReportExtensionIssueAction extends Action {
private static readonly _id = 'workbench.extensions.action.reportExtensionIssue';
private static readonly _label = nls.localize('reportExtensionIssue', "Report Issue");
private _url: string | undefined;
constructor(
private extension: {
description: IExtensionDescription;
marketplaceInfo: IExtension;
status?: IExtensionsStatus;
unresponsiveProfile?: IExtensionHostProfile
},
@IOpenerService private readonly openerService: IOpenerService,
@IClipboardService private readonly clipboardService: IClipboardService,
@IProductService private readonly productService: IProductService,
@INativeHostService private readonly nativeHostService: INativeHostService
) {
super(ReportExtensionIssueAction._id, ReportExtensionIssueAction._label, 'extension-action report-issue');
this.enabled = !!extension.description.repository && !!extension.description.repository.url;
}
async run(): Promise<void> {
if (!this._url) {
this._url = await this._generateNewIssueUrl(this.extension);
}
this.openerService.open(URI.parse(this._url));
}
private async _generateNewIssueUrl(extension: {
description: IExtensionDescription;
marketplaceInfo: IExtension;
status?: IExtensionsStatus;
unresponsiveProfile?: IExtensionHostProfile
}): Promise<string> {
let baseUrl = extension.marketplaceInfo && extension.marketplaceInfo.type === ExtensionType.User && extension.description.repository ? extension.description.repository.url : undefined;
if (!!baseUrl) {
baseUrl = `${baseUrl.indexOf('.git') !== -1 ? baseUrl.substr(0, baseUrl.length - 4) : baseUrl}/issues/new/`;
} else {
baseUrl = this.productService.reportIssueUrl!;
}
let reason = 'Bug';
let title = 'Extension issue';
let message = ':warning: We have written the needed data into your clipboard. Please paste! :warning:';
this.clipboardService.writeText('```json \n' + JSON.stringify(extension.status, null, '\t') + '\n```');
const os = await this.nativeHostService.getOSProperties();
const osVersion = `${os.type} ${os.arch} ${os.release}`;
const queryStringPrefix = baseUrl.indexOf('?') === -1 ? '?' : '&';
const body = encodeURIComponent(
`- Issue Type: \`${reason}\`
- Extension Name: \`${extension.description.name}\`
- Extension Version: \`${extension.description.version}\`
- OS Version: \`${osVersion}\`
- VSCode version: \`${this.productService.version}\`\n\n${message}`
);
return `${baseUrl}${queryStringPrefix}body=${body}&title=${encodeURIComponent(title)}`;
}
}

View File

@@ -3,50 +3,28 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/runtimeExtensionsEditor';
import * as nls from 'vs/nls';
import * as os from 'os';
import { IProductService } from 'vs/platform/product/common/productService';
import { Action, IAction, Separator } from 'vs/base/common/actions';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { Action } from 'vs/base/common/actions';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions';
import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IExtensionService, IExtensionsStatus, IExtensionHostProfile } from 'vs/workbench/services/extensions/common/extensions';
import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list';
import { WorkbenchList } from 'vs/platform/list/browser/listService';
import { append, $, reset, Dimension, clearNode } from 'vs/base/browser/dom';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { RunOnceScheduler } from 'vs/base/common/async';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionService, IExtensionHostProfile } from 'vs/workbench/services/extensions/common/extensions';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
import { writeFile } from 'vs/base/node/pfs';
import { memoize } from 'vs/base/common/decorators';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { Event } from 'vs/base/common/event';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput';
import { IDebugService } from 'vs/workbench/contrib/debug/common/debug';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { randomPort } from 'vs/base/node/ports';
import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ILabelService } from 'vs/platform/label/common/label';
import { renderCodicons } from 'vs/base/browser/codicons';
import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { Schemas } from 'vs/base/common/network';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { SlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { ReportExtensionIssueAction } from 'vs/workbench/contrib/extensions/electron-browser/reportExtensionIssueAction';
import { AbstractRuntimeExtensionsEditor, IRuntimeExtension } from 'vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor';
import { VSBuffer } from 'vs/base/common/buffer';
import { URI } from 'vs/base/common/uri';
import { editorBackground } from 'vs/platform/theme/common/colorRegistry';
import { domEvent } from 'vs/base/browser/event';
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { IFileService } from 'vs/platform/files/common/files';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
export const IExtensionHostProfileService = createDecorator<IExtensionHostProfileService>('extensionHostProfileService');
export const CONTEXT_PROFILE_SESSION_STATE = new RawContextKey<string>('profileSessionState', 'none');
@@ -75,63 +53,31 @@ export interface IExtensionHostProfileService {
setUnresponsiveProfile(extensionId: ExtensionIdentifier, profile: IExtensionHostProfile): void;
}
interface IExtensionProfileInformation {
/**
* segment when the extension was running.
* 2*i = segment start time
* 2*i+1 = segment end time
*/
segments: number[];
/**
* total time when the extension was running.
* (sum of all segment lengths).
*/
totalTime: number;
}
export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor {
interface IRuntimeExtension {
originalIndex: number;
description: IExtensionDescription;
marketplaceInfo: IExtension;
status: IExtensionsStatus;
profileInfo?: IExtensionProfileInformation;
unresponsiveProfile?: IExtensionHostProfile;
}
export class RuntimeExtensionsEditor extends EditorPane {
public static readonly ID: string = 'workbench.editor.runtimeExtensions';
private _list: WorkbenchList<IRuntimeExtension> | null;
private _profileInfo: IExtensionHostProfile | null;
private _elements: IRuntimeExtension[] | null;
private _extensionsDescriptions: IExtensionDescription[];
private _updateSoon: RunOnceScheduler;
private _profileSessionState: IContextKey<string>;
private _extensionsHostRecorded: IContextKey<boolean>;
private _profileSessionState: IContextKey<string>;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IContextKeyService contextKeyService: IContextKeyService,
@IExtensionsWorkbenchService private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionService private readonly _extensionService: IExtensionService,
@INotificationService private readonly _notificationService: INotificationService,
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IExtensionHostProfileService private readonly _extensionHostProfileService: IExtensionHostProfileService,
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionService extensionService: IExtensionService,
@INotificationService notificationService: INotificationService,
@IContextMenuService contextMenuService: IContextMenuService,
@IInstantiationService instantiationService: IInstantiationService,
@IStorageService storageService: IStorageService,
@ILabelService private readonly _labelService: ILabelService,
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
@IOpenerService private readonly _openerService: IOpenerService,
@IClipboardService private readonly _clipboardService: IClipboardService,
@IProductService private readonly _productService: IProductService
@ILabelService labelService: ILabelService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IExtensionHostProfileService private readonly _extensionHostProfileService: IExtensionHostProfileService,
) {
super(RuntimeExtensionsEditor.ID, telemetryService, themeService, storageService);
this._list = null;
super(telemetryService, themeService, contextKeyService, extensionsWorkbenchService, extensionService, notificationService, contextMenuService, instantiationService, storageService, labelService, environmentService);
this._profileInfo = this._extensionHostProfileService.lastProfile;
this._extensionsHostRecorded = CONTEXT_EXTENSION_HOST_PROFILE_RECORDED.bindTo(contextKeyService);
this._profileSessionState = CONTEXT_PROFILE_SESSION_STATE.bindTo(contextKeyService);
this._register(this._extensionHostProfileService.onDidChangeLastProfile(() => {
this._profileInfo = this._extensionHostProfileService.lastProfile;
this._extensionsHostRecorded.set(!!this._profileInfo);
@@ -141,483 +87,39 @@ export class RuntimeExtensionsEditor extends EditorPane {
const state = this._extensionHostProfileService.state;
this._profileSessionState.set(ProfileSessionState[state].toLowerCase());
}));
this._elements = null;
this._extensionsDescriptions = [];
this._updateExtensions();
this._profileSessionState = CONTEXT_PROFILE_SESSION_STATE.bindTo(contextKeyService);
this._extensionsHostRecorded = CONTEXT_EXTENSION_HOST_PROFILE_RECORDED.bindTo(contextKeyService);
this._updateSoon = this._register(new RunOnceScheduler(() => this._updateExtensions(), 200));
this._extensionService.getExtensions().then((extensions) => {
// We only deal with extensions with source code!
this._extensionsDescriptions = extensions.filter((extension) => {
return Boolean(extension.main) || Boolean(extension.browser);
});
this._updateExtensions();
});
this._register(this._extensionService.onDidChangeExtensionsStatus(() => this._updateSoon.schedule()));
}
private async _updateExtensions(): Promise<void> {
this._elements = await this._resolveExtensions();
if (this._list) {
this._list.splice(0, this._list.length, this._elements);
}
protected _getProfileInfo(): IExtensionHostProfile | null {
return this._profileInfo;
}
private async _resolveExtensions(): Promise<IRuntimeExtension[]> {
let marketplaceMap: { [id: string]: IExtension; } = Object.create(null);
const marketPlaceExtensions = await this._extensionsWorkbenchService.queryLocal();
for (let extension of marketPlaceExtensions) {
marketplaceMap[ExtensionIdentifier.toKey(extension.identifier.id)] = extension;
}
let statusMap = this._extensionService.getExtensionsStatus();
// group profile segments by extension
let segments: { [id: string]: number[]; } = Object.create(null);
if (this._profileInfo) {
let currentStartTime = this._profileInfo.startTime;
for (let i = 0, len = this._profileInfo.deltas.length; i < len; i++) {
const id = this._profileInfo.ids[i];
const delta = this._profileInfo.deltas[i];
let extensionSegments = segments[ExtensionIdentifier.toKey(id)];
if (!extensionSegments) {
extensionSegments = [];
segments[ExtensionIdentifier.toKey(id)] = extensionSegments;
}
extensionSegments.push(currentStartTime);
currentStartTime = currentStartTime + delta;
extensionSegments.push(currentStartTime);
}
}
let result: IRuntimeExtension[] = [];
for (let i = 0, len = this._extensionsDescriptions.length; i < len; i++) {
const extensionDescription = this._extensionsDescriptions[i];
let profileInfo: IExtensionProfileInformation | null = null;
if (this._profileInfo) {
let extensionSegments = segments[ExtensionIdentifier.toKey(extensionDescription.identifier)] || [];
let extensionTotalTime = 0;
for (let j = 0, lenJ = extensionSegments.length / 2; j < lenJ; j++) {
const startTime = extensionSegments[2 * j];
const endTime = extensionSegments[2 * j + 1];
extensionTotalTime += (endTime - startTime);
}
profileInfo = {
segments: extensionSegments,
totalTime: extensionTotalTime
};
}
result[i] = {
originalIndex: i,
description: extensionDescription,
marketplaceInfo: marketplaceMap[ExtensionIdentifier.toKey(extensionDescription.identifier)],
status: statusMap[extensionDescription.identifier.value],
profileInfo: profileInfo || undefined,
unresponsiveProfile: this._extensionHostProfileService.getUnresponsiveProfile(extensionDescription.identifier)
};
}
result = result.filter(element => element.status.activationTimes);
// bubble up extensions that have caused slowness
const isUnresponsive = (extension: IRuntimeExtension): boolean =>
extension.unresponsiveProfile === this._profileInfo;
const profileTime = (extension: IRuntimeExtension): number =>
extension.profileInfo?.totalTime ?? 0;
const activationTime = (extension: IRuntimeExtension): number =>
(extension.status.activationTimes?.codeLoadingTime ?? 0) +
(extension.status.activationTimes?.activateCallTime ?? 0);
result = result.sort((a, b) => {
if (isUnresponsive(a) || isUnresponsive(b)) {
return +isUnresponsive(b) - +isUnresponsive(a);
} else if (profileTime(a) || profileTime(b)) {
return profileTime(b) - profileTime(a);
} else if (activationTime(a) || activationTime(b)) {
return activationTime(b) - activationTime(a);
}
return a.originalIndex - b.originalIndex;
});
return result;
protected _getUnresponsiveProfile(extensionId: ExtensionIdentifier): IExtensionHostProfile | undefined {
return this._extensionHostProfileService.getUnresponsiveProfile(extensionId);
}
protected createEditor(parent: HTMLElement): void {
parent.classList.add('runtime-extensions-editor');
const TEMPLATE_ID = 'runtimeExtensionElementTemplate';
const delegate = new class implements IListVirtualDelegate<IRuntimeExtension>{
getHeight(element: IRuntimeExtension): number {
return 62;
}
getTemplateId(element: IRuntimeExtension): string {
return TEMPLATE_ID;
}
};
interface IRuntimeExtensionTemplateData {
root: HTMLElement;
element: HTMLElement;
icon: HTMLImageElement;
name: HTMLElement;
version: HTMLElement;
msgContainer: HTMLElement;
actionbar: ActionBar;
activationTime: HTMLElement;
profileTime: HTMLElement;
disposables: IDisposable[];
elementDisposables: IDisposable[];
protected _createSlowExtensionAction(element: IRuntimeExtension): Action | null {
if (element.unresponsiveProfile) {
return this._instantiationService.createInstance(SlowExtensionAction, element.description, element.unresponsiveProfile);
}
const renderer: IListRenderer<IRuntimeExtension, IRuntimeExtensionTemplateData> = {
templateId: TEMPLATE_ID,
renderTemplate: (root: HTMLElement): IRuntimeExtensionTemplateData => {
const element = append(root, $('.extension'));
const iconContainer = append(element, $('.icon-container'));
const icon = append(iconContainer, $<HTMLImageElement>('img.icon'));
const desc = append(element, $('div.desc'));
const headerContainer = append(desc, $('.header-container'));
const header = append(headerContainer, $('.header'));
const name = append(header, $('div.name'));
const version = append(header, $('span.version'));
const msgContainer = append(desc, $('div.msg'));
const actionbar = new ActionBar(desc, { animated: false });
actionbar.onDidRun(({ error }) => error && this._notificationService.error(error));
const timeContainer = append(element, $('.time'));
const activationTime = append(timeContainer, $('div.activation-time'));
const profileTime = append(timeContainer, $('div.profile-time'));
const disposables = [actionbar];
return {
root,
element,
icon,
name,
version,
actionbar,
activationTime,
profileTime,
msgContainer,
disposables,
elementDisposables: [],
};
},
renderElement: (element: IRuntimeExtension, index: number, data: IRuntimeExtensionTemplateData): void => {
data.elementDisposables = dispose(data.elementDisposables);
data.root.classList.toggle('odd', index % 2 === 1);
const onError = Event.once(domEvent(data.icon, 'error'));
onError(() => data.icon.src = element.marketplaceInfo.iconUrlFallback, null, data.elementDisposables);
data.icon.src = element.marketplaceInfo.iconUrl;
if (!data.icon.complete) {
data.icon.style.visibility = 'hidden';
data.icon.onload = () => data.icon.style.visibility = 'inherit';
} else {
data.icon.style.visibility = 'inherit';
}
data.name.textContent = element.marketplaceInfo.displayName;
data.version.textContent = element.description.version;
const activationTimes = element.status.activationTimes!;
let syncTime = activationTimes.codeLoadingTime + activationTimes.activateCallTime;
data.activationTime.textContent = activationTimes.activationReason.startup ? `Startup Activation: ${syncTime}ms` : `Activation: ${syncTime}ms`;
data.actionbar.clear();
if (element.unresponsiveProfile) {
data.actionbar.push(this._instantiationService.createInstance(SlowExtensionAction, element.description, element.unresponsiveProfile), { icon: true, label: true });
}
if (isNonEmptyArray(element.status.runtimeErrors)) {
data.actionbar.push(new ReportExtensionIssueAction(element, this._openerService, this._clipboardService, this._productService), { icon: true, label: true });
}
let title: string;
const activationId = activationTimes.activationReason.extensionId.value;
const activationEvent = activationTimes.activationReason.activationEvent;
if (activationEvent === '*') {
title = nls.localize('starActivation', "Activated by {0} on start-up", activationId);
} else if (/^workspaceContains:/.test(activationEvent)) {
let fileNameOrGlob = activationEvent.substr('workspaceContains:'.length);
if (fileNameOrGlob.indexOf('*') >= 0 || fileNameOrGlob.indexOf('?') >= 0) {
title = nls.localize({
key: 'workspaceContainsGlobActivation',
comment: [
'{0} will be a glob pattern'
]
}, "Activated by {1} because a file matching {1} exists in your workspace", fileNameOrGlob, activationId);
} else {
title = nls.localize({
key: 'workspaceContainsFileActivation',
comment: [
'{0} will be a file name'
]
}, "Activated by {1} because file {0} exists in your workspace", fileNameOrGlob, activationId);
}
} else if (/^workspaceContainsTimeout:/.test(activationEvent)) {
const glob = activationEvent.substr('workspaceContainsTimeout:'.length);
title = nls.localize({
key: 'workspaceContainsTimeout',
comment: [
'{0} will be a glob pattern'
]
}, "Activated by {1} because searching for {0} took too long", glob, activationId);
} else if (activationEvent === 'onStartupFinished') {
title = nls.localize({
key: 'startupFinishedActivation',
comment: [
'This refers to an extension. {0} will be an activation event.'
]
}, "Activated by {0} after start-up finished", activationId);
} else if (/^onLanguage:/.test(activationEvent)) {
let language = activationEvent.substr('onLanguage:'.length);
title = nls.localize('languageActivation', "Activated by {1} because you opened a {0} file", language, activationId);
} else {
title = nls.localize({
key: 'workspaceGenericActivation',
comment: [
'The {0} placeholder will be an activation event, like e.g. \'language:typescript\', \'debug\', etc.'
]
}, "Activated by {1} on {0}", activationEvent, activationId);
}
data.activationTime.title = title;
clearNode(data.msgContainer);
if (this._extensionHostProfileService.getUnresponsiveProfile(element.description.identifier)) {
const el = $('span', undefined, ...renderCodicons(` $(alert) Unresponsive`));
el.title = nls.localize('unresponsive.title', "Extension has caused the extension host to freeze.");
data.msgContainer.appendChild(el);
}
if (isNonEmptyArray(element.status.runtimeErrors)) {
const el = $('span', undefined, ...renderCodicons(`$(bug) ${nls.localize('errors', "{0} uncaught errors", element.status.runtimeErrors.length)}`));
data.msgContainer.appendChild(el);
}
if (element.status.messages && element.status.messages.length > 0) {
const el = $('span', undefined, ...renderCodicons(`$(alert) ${element.status.messages[0].message}`));
data.msgContainer.appendChild(el);
}
if (element.description.extensionLocation.scheme !== 'file') {
const el = $('span', undefined, ...renderCodicons(`$(remote) ${element.description.extensionLocation.authority}`));
data.msgContainer.appendChild(el);
const hostLabel = this._labelService.getHostLabel(Schemas.vscodeRemote, this._environmentService.configuration.remoteAuthority);
if (hostLabel) {
reset(el, ...renderCodicons(`$(remote) ${hostLabel}`));
}
}
if (this._profileInfo && element.profileInfo) {
data.profileTime.textContent = `Profile: ${(element.profileInfo.totalTime / 1000).toFixed(2)}ms`;
} else {
data.profileTime.textContent = '';
}
},
disposeTemplate: (data: IRuntimeExtensionTemplateData): void => {
data.disposables = dispose(data.disposables);
}
};
this._list = <WorkbenchList<IRuntimeExtension>>this._instantiationService.createInstance(WorkbenchList,
'RuntimeExtensions',
parent, delegate, [renderer], {
multipleSelectionSupport: false,
setRowLineHeight: false,
horizontalScrolling: false,
overrideStyles: {
listBackground: editorBackground
},
accessibilityProvider: new RuntimeExtensionsEditorAccessibilityProvider()
});
this._list.splice(0, this._list.length, this._elements || undefined);
this._list.onContextMenu((e) => {
if (!e.element) {
return;
}
const actions: IAction[] = [];
actions.push(new ReportExtensionIssueAction(e.element, this._openerService, this._clipboardService, this._productService));
actions.push(new Separator());
actions.push(new Action('runtimeExtensionsEditor.action.disableWorkspace', nls.localize('disable workspace', "Disable (Workspace)"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo, EnablementState.DisabledWorkspace)));
actions.push(new Action('runtimeExtensionsEditor.action.disable', nls.localize('disable', "Disable"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo, EnablementState.DisabledGlobally)));
actions.push(new Separator());
const state = this._extensionHostProfileService.state;
if (state === ProfileSessionState.Running) {
actions.push(this._instantiationService.createInstance(StopExtensionHostProfileAction, StopExtensionHostProfileAction.ID, StopExtensionHostProfileAction.LABEL));
} else {
actions.push(this._instantiationService.createInstance(StartExtensionHostProfileAction, StartExtensionHostProfileAction.ID, StartExtensionHostProfileAction.LABEL));
}
actions.push(this.saveExtensionHostProfileAction);
this._contextMenuService.showContextMenu({
getAnchor: () => e.anchor,
getActions: () => actions
});
});
return null;
}
@memoize
private get saveExtensionHostProfileAction(): IAction {
protected _createReportExtensionIssueAction(element: IRuntimeExtension): Action | null {
return this._instantiationService.createInstance(ReportExtensionIssueAction, element);
}
protected _createSaveExtensionHostProfileAction(): Action | null {
return this._instantiationService.createInstance(SaveExtensionHostProfileAction, SaveExtensionHostProfileAction.ID, SaveExtensionHostProfileAction.LABEL);
}
public layout(dimension: Dimension): void {
if (this._list) {
this._list.layout(dimension.height);
}
}
}
export class ShowRuntimeExtensionsAction extends Action {
static readonly ID = 'workbench.action.showRuntimeExtensions';
static readonly LABEL = nls.localize('showRuntimeExtensions', "Show Running Extensions");
constructor(
id: string, label: string,
@IEditorService private readonly _editorService: IEditorService
) {
super(id, label);
}
public async run(e?: any): Promise<any> {
await this._editorService.openEditor(RuntimeExtensionsInput.instance, { revealIfOpened: true });
}
}
export class ReportExtensionIssueAction extends Action {
private static readonly _id = 'workbench.extensions.action.reportExtensionIssue';
private static readonly _label = nls.localize('reportExtensionIssue', "Report Issue");
private readonly _url: string;
constructor(
extension: {
description: IExtensionDescription;
marketplaceInfo: IExtension;
status?: IExtensionsStatus;
unresponsiveProfile?: IExtensionHostProfile
},
@IOpenerService private readonly openerService: IOpenerService,
@IClipboardService private readonly clipboardService: IClipboardService,
@IProductService private readonly productService: IProductService
) {
super(ReportExtensionIssueAction._id, ReportExtensionIssueAction._label, 'extension-action report-issue');
this.enabled = extension.marketplaceInfo
&& extension.marketplaceInfo.type === ExtensionType.User
&& !!extension.description.repository && !!extension.description.repository.url;
this._url = this._generateNewIssueUrl(extension);
}
async run(): Promise<void> {
this.openerService.open(URI.parse(this._url));
}
private _generateNewIssueUrl(extension: {
description: IExtensionDescription;
marketplaceInfo: IExtension;
status?: IExtensionsStatus;
unresponsiveProfile?: IExtensionHostProfile
}): string {
let baseUrl = extension.marketplaceInfo && extension.marketplaceInfo.type === ExtensionType.User && extension.description.repository ? extension.description.repository.url : undefined;
if (!!baseUrl) {
baseUrl = `${baseUrl.indexOf('.git') !== -1 ? baseUrl.substr(0, baseUrl.length - 4) : baseUrl}/issues/new/`;
} else {
baseUrl = this.productService.reportIssueUrl!;
}
let reason = 'Bug';
let title = 'Extension issue';
let message = ':warning: We have written the needed data into your clipboard. Please paste! :warning:';
this.clipboardService.writeText('```json \n' + JSON.stringify(extension.status, null, '\t') + '\n```');
const osVersion = `${os.type()} ${os.arch()} ${os.release()}`;
const queryStringPrefix = baseUrl.indexOf('?') === -1 ? '?' : '&';
const body = encodeURIComponent(
`- Issue Type: \`${reason}\`
- Extension Name: \`${extension.description.name}\`
- Extension Version: \`${extension.description.version}\`
- OS Version: \`${osVersion}\`
- VSCode version: \`${this.productService.version}\`\n\n${message}`
protected _createProfileAction(): Action | null {
const state = this._extensionHostProfileService.state;
const profileAction = (
state === ProfileSessionState.Running
? this._instantiationService.createInstance(StopExtensionHostProfileAction, StopExtensionHostProfileAction.ID, StopExtensionHostProfileAction.LABEL)
: this._instantiationService.createInstance(StartExtensionHostProfileAction, StartExtensionHostProfileAction.ID, StartExtensionHostProfileAction.LABEL)
);
return `${baseUrl}${queryStringPrefix}body=${body}&title=${encodeURIComponent(title)}`;
}
}
export class DebugExtensionHostAction extends Action {
static readonly ID = 'workbench.extensions.action.debugExtensionHost';
static readonly LABEL = nls.localize('debugExtensionHost', "Start Debugging Extension Host");
static readonly CSS_CLASS = 'debug-extension-host';
constructor(
@IDebugService private readonly _debugService: IDebugService,
@IElectronService private readonly _electronService: IElectronService,
@IDialogService private readonly _dialogService: IDialogService,
@IExtensionService private readonly _extensionService: IExtensionService,
@IProductService private readonly productService: IProductService
) {
super(DebugExtensionHostAction.ID, DebugExtensionHostAction.LABEL, DebugExtensionHostAction.CSS_CLASS);
}
async run(): Promise<any> {
const inspectPort = await this._extensionService.getInspectPort(false);
if (!inspectPort) {
const res = await this._dialogService.confirm({
type: 'info',
message: nls.localize('restart1', "Profile Extensions"),
detail: nls.localize('restart2', "In order to profile extensions a restart is required. Do you want to restart '{0}' now?", this.productService.nameLong),
primaryButton: nls.localize('restart3', "Restart"),
secondaryButton: nls.localize('cancel', "Cancel")
});
if (res.confirmed) {
await this._electronService.relaunch({ addArgs: [`--inspect-extensions=${randomPort()}`] });
}
return;
}
return this._debugService.startDebugging(undefined, {
type: 'node',
name: nls.localize('debugExtensionHost.launch.name', "Attach Extension Host"),
request: 'attach',
port: inspectPort
});
return profileAction;
}
}
@@ -662,9 +164,10 @@ export class SaveExtensionHostProfileAction extends Action {
constructor(
id: string = SaveExtensionHostProfileAction.ID, label: string = SaveExtensionHostProfileAction.LABEL,
@IElectronService private readonly _electronService: IElectronService,
@INativeHostService private readonly _nativeHostService: INativeHostService,
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
@IExtensionHostProfileService private readonly _extensionHostProfileService: IExtensionHostProfileService,
@IFileService private readonly _fileService: IFileService
) {
super(id, label, undefined, false);
this._extensionHostProfileService.onDidChangeLastProfile(() => {
@@ -677,7 +180,7 @@ export class SaveExtensionHostProfileAction extends Action {
}
private async _asyncRun(): Promise<any> {
let picked = await this._electronService.showSaveDialog({
let picked = await this._nativeHostService.showSaveDialog({
title: 'Save Extension Host Profile',
buttonLabel: 'Save',
defaultPath: `CPU-${new Date().toISOString().replace(/[\-:]/g, '')}.cpuprofile`,
@@ -702,23 +205,12 @@ export class SaveExtensionHostProfileAction extends Action {
// absolute filenames because we don't want to reveal anything
// about users. We also append the `.txt` suffix to make it
// easier to attach these files to GH issues
let tmp = profiler.rewriteAbsolutePaths({ profile: dataToWrite as any }, 'piiRemoved');
dataToWrite = tmp.profile;
savePath = savePath + '.txt';
}
return writeFile(savePath, JSON.stringify(profileInfo ? profileInfo.data : {}, null, '\t'));
}
}
class RuntimeExtensionsEditorAccessibilityProvider implements IListAccessibilityProvider<IRuntimeExtension> {
getWidgetAriaLabel(): string {
return nls.localize('runtimeExtensions', "Runtime Extensions");
}
getAriaLabel(element: IRuntimeExtension): string | null {
return element.description.name;
return this._fileService.writeFile(URI.file(savePath), VSBuffer.fromString(JSON.stringify(profileInfo ? profileInfo.data : {}, null, '\t')));
}
}

View File

@@ -7,9 +7,8 @@ import { localize } from 'vs/nls';
import { Action } from 'vs/base/common/actions';
import { IFileService } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
import { Schemas } from 'vs/base/common/network';
export class OpenExtensionsFolderAction extends Action {
@@ -20,28 +19,26 @@ export class OpenExtensionsFolderAction extends Action {
constructor(
id: string,
label: string,
@IElectronService private readonly electronService: IElectronService,
@INativeHostService private readonly nativeHostService: INativeHostService,
@IFileService private readonly fileService: IFileService,
@IWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService
@INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService
) {
super(id, label, undefined, true);
}
async run(): Promise<void> {
if (this.environmentService.extensionsPath) {
const extensionsHome = URI.file(this.environmentService.extensionsPath);
const file = await this.fileService.resolve(extensionsHome);
const extensionsHome = URI.file(this.environmentService.extensionsPath);
const file = await this.fileService.resolve(extensionsHome);
let itemToShow: URI;
if (file.children && file.children.length > 0) {
itemToShow = file.children[0].resource;
} else {
itemToShow = extensionsHome;
}
let itemToShow: URI;
if (file.children && file.children.length > 0) {
itemToShow = file.children[0].resource;
} else {
itemToShow = extensionsHome;
}
if (itemToShow.scheme === Schemas.file) {
return this.electronService.showItemInFolder(itemToShow.fsPath);
}
if (itemToShow.scheme === Schemas.file) {
return this.nativeHostService.showItemInFolder(itemToShow.fsPath);
}
}
}

View File

@@ -17,7 +17,7 @@ import {
import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { Emitter } from 'vs/base/common/event';
import { Emitter, Event } from 'vs/base/common/event';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
@@ -37,12 +37,12 @@ import { TestExtensionEnablementService } from 'vs/workbench/services/extensionM
import { IURLService } from 'vs/platform/url/common/url';
import { ITextModel } from 'vs/editor/common/model';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { INotificationService, Severity, IPromptChoice, IPromptOptions } from 'vs/platform/notification/common/notification';
import { NativeURLService } from 'vs/platform/url/common/urlService';
import { IExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService';
import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-browser/experimentService.test';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ExtensionType } from 'vs/platform/extensions/common/extensions';
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
import { FileService } from 'vs/platform/files/common/fileService';
@@ -51,13 +51,17 @@ import { Schemas } from 'vs/base/common/network';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { IFileService } from 'vs/platform/files/common/files';
import { IProductService } from 'vs/platform/product/common/productService';
import { ExtensionTipsService } from 'vs/platform/extensionManagement/node/extensionTipsService';
import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-sandbox/extensionTipsService';
import { ExtensionRecommendationsService } from 'vs/workbench/contrib/extensions/browser/extensionRecommendationsService';
import { NoOpWorkspaceTagsService } from 'vs/workbench/contrib/tags/browser/workspaceTagsService';
import { IWorkspaceTagsService } from 'vs/workbench/contrib/tags/common/workspaceTags';
import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IWorkpsaceExtensionsConfigService, WorkspaceExtensionsConfigService } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig';
import { IExtensionIgnoredRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { ExtensionIgnoredRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionIgnoredRecommendationsService';
import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
import { ExtensionRecommendationNotificationService } from 'vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService';
const mockExtensionGallery: IGalleryExtension[] = [
aGalleryExtension('MockExtension1', {
@@ -183,6 +187,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
uninstallEvent: Emitter<IExtensionIdentifier>,
didUninstallEvent: Emitter<DidUninstallExtensionEvent>;
let prompted: boolean;
let promptedEmitter = new Emitter<void>();
let onModelAddedEvent: Emitter<ITextModel>;
let experimentService: TestExperimentService;
@@ -216,7 +221,6 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
instantiationService.stub(IWorkspaceTagsService, new NoOpWorkspaceTagsService());
instantiationService.stub(IStorageService, new TestStorageService());
instantiationService.stub(ILogService, new NullLogService());
instantiationService.stub(IStorageKeysSyncRegistryService, new StorageKeysSyncRegistryService());
instantiationService.stub(IProductService, <Partial<IProductService>>{
extensionTips: {
'ms-dotnettools.csharp': '{**/*.cs,**/project.json,**/global.json,**/*.csproj,**/*.sln,**/appsettings.json}',
@@ -260,13 +264,14 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
class TestNotificationService2 extends TestNotificationService {
public prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions) {
prompted = true;
return null!;
promptedEmitter.fire();
return super.prompt(severity, message, choices, options);
}
}
instantiationService.stub(INotificationService, new TestNotificationService2());
testConfigurationService.setUserConfiguration(ConfigurationKey, { ignoreRecommendations: false, showRecommendationsOnlyOnDemand: false });
testConfigurationService.setUserConfiguration(ConfigurationKey, { ignoreRecommendations: false });
instantiationService.stub(IModelService, <IModelService>{
getModels(): any { return []; },
onModelAdded: onModelAddedEvent.event
@@ -299,11 +304,14 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
}, null, '\t'));
const myWorkspace = testWorkspace(URI.from({ scheme: 'file', path: folderDir }));
workspaceService = new TestContextService(myWorkspace);
instantiationService.stub(IWorkspaceContextService, workspaceService);
const fileService = new FileService(new NullLogService());
fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService()));
instantiationService.stub(IFileService, fileService);
workspaceService = new TestContextService(myWorkspace);
instantiationService.stub(IWorkspaceContextService, workspaceService);
instantiationService.stub(IWorkpsaceExtensionsConfigService, instantiationService.createInstance(WorkspaceExtensionsConfigService));
instantiationService.stub(IExtensionIgnoredRecommendationsService, instantiationService.createInstance(ExtensionIgnoredRecommendationsService));
instantiationService.stub(IExtensionRecommendationNotificationService, instantiationService.createInstance(ExtensionRecommendationNotificationService));
}
function testNoPromptForValidRecommendations(recommendations: string[]) {
@@ -348,15 +356,14 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
test('ExtensionRecommendationsService: Prompt for valid workspace recommendations', async () => {
await setUpFolderWorkspace('myFolder', mockTestData.recommendedExtensions);
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
await testObject.activationPromise;
await Event.toPromise(promptedEmitter.event);
const recommendations = Object.keys(testObject.getAllRecommendationsWithReason());
assert.equal(recommendations.length, mockTestData.validRecommendedExtensions.length);
mockTestData.validRecommendedExtensions.forEach(x => {
assert.equal(recommendations.indexOf(x.toLowerCase()) > -1, true);
});
assert.ok(prompted);
});
test('ExtensionRecommendationsService: No Prompt for valid workspace recommendations if they are already installed', () => {
@@ -374,6 +381,16 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
return testNoPromptForValidRecommendations(mockTestData.validRecommendedExtensions);
});
test('ExtensionRecommendationsService: No Prompt for valid workspace recommendations if ignoreRecommendations is set', () => {
testConfigurationService.setUserConfiguration(ConfigurationKey, { ignoreRecommendations: true });
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.activationPromise.then(() => {
assert.ok(!prompted);
});
});
});
test('ExtensionRecommendationsService: No Prompt for valid workspace recommendations if showRecommendationsOnlyOnDemand is set', () => {
testConfigurationService.setUserConfiguration(ConfigurationKey, { showRecommendationsOnlyOnDemand: true });
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => {
@@ -385,14 +402,14 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
});
test('ExtensionRecommendationsService: No Prompt for valid workspace recommendations if ignoreRecommendations is set for current workspace', () => {
instantiationService.get(IStorageService).store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE);
instantiationService.get(IStorageService).store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE, StorageTarget.MACHINE);
return testNoPromptForValidRecommendations(mockTestData.validRecommendedExtensions);
});
test('ExtensionRecommendationsService: No Recommendations of globally ignored recommendations', () => {
instantiationService.get(IStorageService).store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE);
instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', '["ms-dotnettools.csharp", "ms-python.python", "ms-vscode.vscode-typescript-tslint-plugin"]', StorageScope.GLOBAL);
instantiationService.get(IStorageService).store('extensionsAssistant/ignored_recommendations', '["ms-dotnettools.csharp", "mockpublisher2.mockextension2"]', StorageScope.GLOBAL);
instantiationService.get(IStorageService).store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE, StorageTarget.MACHINE);
instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', '["ms-dotnettools.csharp", "ms-python.python", "ms-vscode.vscode-typescript-tslint-plugin"]', StorageScope.GLOBAL, StorageTarget.MACHINE);
instantiationService.get(IStorageService).store('extensionsAssistant/ignored_recommendations', '["ms-dotnettools.csharp", "mockpublisher2.mockextension2"]', StorageScope.GLOBAL, StorageTarget.MACHINE);
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
@@ -409,8 +426,8 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
test('ExtensionRecommendationsService: No Recommendations of workspace ignored recommendations', () => {
const ignoredRecommendations = ['ms-dotnettools.csharp', 'mockpublisher2.mockextension2']; // ignore a stored recommendation and a workspace recommendation.
const storedRecommendations = '["ms-dotnettools.csharp", "ms-python.python"]';
instantiationService.get(IStorageService).store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE);
instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL);
instantiationService.get(IStorageService).store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE, StorageTarget.MACHINE);
instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE);
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, ignoredRecommendations).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
@@ -424,73 +441,73 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
});
});
test('ExtensionRecommendationsService: Able to retrieve collection of all ignored recommendations', () => {
test.skip('ExtensionRecommendationsService: Able to retrieve collection of all ignored recommendations', async () => {
const storageService = instantiationService.get(IStorageService);
const workspaceIgnoredRecommendations = ['ms-dotnettools.csharp']; // ignore a stored recommendation and a workspace recommendation.
const storedRecommendations = '["ms-dotnettools.csharp", "ms-python.python"]';
const globallyIgnoredRecommendations = '["mockpublisher2.mockextension2"]'; // ignore a workspace recommendation.
instantiationService.get(IStorageService).store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE);
instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL);
instantiationService.get(IStorageService).store('extensionsAssistant/ignored_recommendations', globallyIgnoredRecommendations, StorageScope.GLOBAL);
storageService.store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE, StorageTarget.MACHINE);
storageService.store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE);
storageService.store('extensionsAssistant/ignored_recommendations', globallyIgnoredRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE);
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, workspaceIgnoredRecommendations).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.activationPromise.then(() => {
const recommendations = testObject.getAllRecommendationsWithReason();
assert.ok(recommendations['ms-python.python']);
await setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, workspaceIgnoredRecommendations);
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
await testObject.activationPromise;
assert.ok(!recommendations['mockpublisher2.mockextension2']);
assert.ok(!recommendations['ms-dotnettools.csharp']);
});
});
const recommendations = testObject.getAllRecommendationsWithReason();
assert.ok(recommendations['ms-python.python'], 'ms-python.python extension shall exist');
assert.ok(!recommendations['mockpublisher2.mockextension2'], 'mockpublisher2.mockextension2 extension shall not exist');
assert.ok(!recommendations['ms-dotnettools.csharp'], 'ms-dotnettools.csharp extension shall not exist');
});
test('ExtensionRecommendationsService: Able to dynamically ignore/unignore global recommendations', () => {
test('ExtensionRecommendationsService: Able to dynamically ignore/unignore global recommendations', async () => {
const storageService = instantiationService.get(IStorageService);
const storedRecommendations = '["ms-dotnettools.csharp", "ms-python.python"]';
const globallyIgnoredRecommendations = '["mockpublisher2.mockextension2"]'; // ignore a workspace recommendation.
instantiationService.get(IStorageService).store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE);
instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL);
instantiationService.get(IStorageService).store('extensionsAssistant/ignored_recommendations', globallyIgnoredRecommendations, StorageScope.GLOBAL);
storageService.store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE, StorageTarget.MACHINE);
storageService.store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE);
storageService.store('extensionsAssistant/ignored_recommendations', globallyIgnoredRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE);
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.activationPromise.then(() => {
const recommendations = testObject.getAllRecommendationsWithReason();
assert.ok(recommendations['ms-python.python']);
assert.ok(recommendations['mockpublisher1.mockextension1']);
await setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions);
const extensionIgnoredRecommendationsService = instantiationService.get(IExtensionIgnoredRecommendationsService);
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
await testObject.activationPromise;
assert.ok(!recommendations['mockpublisher2.mockextension2']);
let recommendations = testObject.getAllRecommendationsWithReason();
assert.ok(recommendations['ms-python.python']);
assert.ok(recommendations['mockpublisher1.mockextension1']);
assert.ok(!recommendations['mockpublisher2.mockextension2']);
return testObject.toggleIgnoredRecommendation('mockpublisher1.mockextension1', true);
}).then(() => {
const recommendations = testObject.getAllRecommendationsWithReason();
assert.ok(recommendations['ms-python.python']);
extensionIgnoredRecommendationsService.toggleGlobalIgnoredRecommendation('mockpublisher1.mockextension1', true);
assert.ok(!recommendations['mockpublisher1.mockextension1']);
assert.ok(!recommendations['mockpublisher2.mockextension2']);
recommendations = testObject.getAllRecommendationsWithReason();
assert.ok(recommendations['ms-python.python']);
assert.ok(!recommendations['mockpublisher1.mockextension1']);
assert.ok(!recommendations['mockpublisher2.mockextension2']);
return testObject.toggleIgnoredRecommendation('mockpublisher1.mockextension1', false);
}).then(() => {
const recommendations = testObject.getAllRecommendationsWithReason();
assert.ok(recommendations['ms-python.python']);
extensionIgnoredRecommendationsService.toggleGlobalIgnoredRecommendation('mockpublisher1.mockextension1', false);
assert.ok(recommendations['mockpublisher1.mockextension1']);
assert.ok(!recommendations['mockpublisher2.mockextension2']);
});
});
recommendations = testObject.getAllRecommendationsWithReason();
assert.ok(recommendations['ms-python.python']);
assert.ok(recommendations['mockpublisher1.mockextension1']);
assert.ok(!recommendations['mockpublisher2.mockextension2']);
});
test('test global extensions are modified and recommendation change event is fired when an extension is ignored', async () => {
const storageService = instantiationService.get(IStorageService);
const changeHandlerTarget = sinon.spy();
const ignoredExtensionId = 'Some.Extension';
instantiationService.get(IStorageService).store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE);
instantiationService.get(IStorageService).store('extensionsAssistant/ignored_recommendations', '["ms-vscode.vscode"]', StorageScope.GLOBAL);
storageService.store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE, StorageTarget.MACHINE);
storageService.store('extensionsAssistant/ignored_recommendations', '["ms-vscode.vscode"]', StorageScope.GLOBAL, StorageTarget.MACHINE);
await setUpFolderWorkspace('myFolder', []);
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
testObject.onRecommendationChange(changeHandlerTarget);
testObject.toggleIgnoredRecommendation(ignoredExtensionId, true);
const extensionIgnoredRecommendationsService = instantiationService.get(IExtensionIgnoredRecommendationsService);
extensionIgnoredRecommendationsService.onDidChangeGlobalIgnoredRecommendation(changeHandlerTarget);
extensionIgnoredRecommendationsService.toggleGlobalIgnoredRecommendation(ignoredExtensionId, true);
await testObject.activationPromise;
assert.ok(changeHandlerTarget.calledOnce);
@@ -499,16 +516,16 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
test('ExtensionRecommendationsService: Get file based recommendations from storage (old format)', () => {
const storedRecommendations = '["ms-dotnettools.csharp", "ms-python.python", "ms-vscode.vscode-typescript-tslint-plugin"]';
instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL);
instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE);
return setUpFolderWorkspace('myFolder', []).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.activationPromise.then(() => {
const recommendations = testObject.getFileBasedRecommendations();
assert.equal(recommendations.length, 2);
assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-dotnettools.csharp')); // stored recommendation that exists in product.extensionTips
assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-python.python')); // stored recommendation that exists in product.extensionImportantTips
assert.ok(recommendations.every(({ extensionId }) => extensionId !== 'ms-vscode.vscode-typescript-tslint-plugin')); // stored recommendation that is no longer in neither product.extensionTips nor product.extensionImportantTips
assert.ok(recommendations.some(extensionId => extensionId === 'ms-dotnettools.csharp')); // stored recommendation that exists in product.extensionTips
assert.ok(recommendations.some(extensionId => extensionId === 'ms-python.python')); // stored recommendation that exists in product.extensionImportantTips
assert.ok(recommendations.every(extensionId => extensionId !== 'ms-vscode.vscode-typescript-tslint-plugin')); // stored recommendation that is no longer in neither product.extensionTips nor product.extensionImportantTips
});
});
});
@@ -518,17 +535,17 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
const now = Date.now();
const tenDaysOld = 10 * milliSecondsInADay;
const storedRecommendations = `{"ms-dotnettools.csharp": ${now}, "ms-python.python": ${now}, "ms-vscode.vscode-typescript-tslint-plugin": ${now}, "lukehoban.Go": ${tenDaysOld}}`;
instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL);
instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE);
return setUpFolderWorkspace('myFolder', []).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.activationPromise.then(() => {
const recommendations = testObject.getFileBasedRecommendations();
assert.equal(recommendations.length, 2);
assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-dotnettools.csharp')); // stored recommendation that exists in product.extensionTips
assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-python.python')); // stored recommendation that exists in product.extensionImportantTips
assert.ok(recommendations.every(({ extensionId }) => extensionId !== 'ms-vscode.vscode-typescript-tslint-plugin')); // stored recommendation that is no longer in neither product.extensionTips nor product.extensionImportantTips
assert.ok(recommendations.every(({ extensionId }) => extensionId !== 'lukehoban.Go')); //stored recommendation that is older than a week
assert.ok(recommendations.some(extensionId => extensionId === 'ms-dotnettools.csharp')); // stored recommendation that exists in product.extensionTips
assert.ok(recommendations.some(extensionId => extensionId === 'ms-python.python')); // stored recommendation that exists in product.extensionImportantTips
assert.ok(recommendations.every(extensionId => extensionId !== 'ms-vscode.vscode-typescript-tslint-plugin')); // stored recommendation that is no longer in neither product.extensionTips nor product.extensionImportantTips
assert.ok(recommendations.every(extensionId => extensionId !== 'lukehoban.Go')); //stored recommendation that is older than a week
});
});
});

View File

@@ -12,7 +12,8 @@ import {
IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension,
DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, InstallOperation, IExtensionTipsService, IGalleryMetadata
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IExtensionRecommendationsService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test';
import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
@@ -22,7 +23,7 @@ import { Emitter, Event } from 'vs/base/common/event';
import { IPager } from 'vs/base/common/paging';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IExtensionService, toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices';
import { TestSharedProcessService } from 'vs/workbench/test/electron-browser/workbenchTestServices';
@@ -37,19 +38,21 @@ import { ExtensionIdentifier, IExtensionContributions, ExtensionType, IExtension
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
import { CancellationToken } from 'vs/base/common/cancellation';
import { ILabelService, IFormatterChangeEvent } from 'vs/platform/label/common/label';
import { ExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService';
import { IProductService } from 'vs/platform/product/common/productService';
import { Schemas } from 'vs/base/common/network';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IProgressService } from 'vs/platform/progress/common/progress';
import { ProgressService } from 'vs/workbench/services/progress/browser/progressService';
import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-browser/experimentService.test';
import { IExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService';
import { ExtensionTipsService } from 'vs/platform/extensionManagement/node/extensionTipsService';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-sandbox/extensionTipsService';
import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { UserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataAutoSyncService';
import { IUserDataAutoSyncEnablementService, IUserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
import { UserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSyncResourceEnablementService';
let instantiationService: TestInstantiationService;
let installEvent: Emitter<InstallExtensionEvent>,
@@ -73,7 +76,6 @@ async function setupTest() {
instantiationService.stub(IWorkspaceContextService, new TestContextService());
instantiationService.stub(IConfigurationService, new TestConfigurationService());
instantiationService.stub(IProgressService, ProgressService);
instantiationService.stub(IStorageKeysSyncRegistryService, new StorageKeysSyncRegistryService());
instantiationService.stub(IProductService, {});
instantiationService.stub(IExtensionGalleryService, ExtensionGalleryService);
@@ -96,14 +98,18 @@ async function setupTest() {
instantiationService.stub(IRemoteAgentService, RemoteAgentService);
instantiationService.stub(IExtensionManagementServerService, new class extends ExtensionManagementServerService {
#localExtensionManagementServer: IExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local', id: 'vscode-local' };
constructor() {
super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService), instantiationService.get(ILabelService), instantiationService.get(IExtensionGalleryService), instantiationService.get(IProductService), instantiationService.get(IConfigurationService), instantiationService.get(ILogService));
const localExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local', id: 'vscode-local' };
instantiationService.stub(IExtensionManagementServerService, <Partial<IExtensionManagementServerService>>{
get localExtensionManagementServer(): IExtensionManagementServer {
return localExtensionManagementServer;
},
getExtensionManagementServer(extension: IExtension): IExtensionManagementServer | null {
if (extension.location.scheme === Schemas.file) {
return localExtensionManagementServer;
}
throw new Error(`Invalid Extension ${extension.location}`);
}
get localExtensionManagementServer(): IExtensionManagementServer { return this.#localExtensionManagementServer; }
set localExtensionManagementServer(server: IExtensionManagementServer) { }
}());
});
instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService));
instantiationService.stub(ILabelService, { onDidChangeFormatters: new Emitter<IFormatterChangeEvent>().event });
@@ -118,6 +124,9 @@ async function setupTest() {
instantiationService.stub(IExtensionService, <Partial<IExtensionService>>{ getExtensions: () => Promise.resolve([]), onDidChangeExtensions: new Emitter<void>().event, canAddExtension: (extension: IExtensionDescription) => false, canRemoveExtension: (extension: IExtensionDescription) => false });
(<TestExtensionEnablementService>instantiationService.get(IWorkbenchExtensionEnablementService)).reset();
instantiationService.stub(IUserDataAutoSyncEnablementService, instantiationService.createInstance(UserDataAutoSyncEnablementService));
instantiationService.stub(IUserDataSyncResourceEnablementService, instantiationService.createInstance(UserDataSyncResourceEnablementService));
instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService)));
}
@@ -152,9 +161,9 @@ suite('ExtensionsActions', () => {
});
});
test('Test Install action when state is installing', () => {
test('Test InstallingLabelAction when state is installing', () => {
const workbenchService = instantiationService.get(IExtensionsWorkbenchService);
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction);
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallingLabelAction);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const gallery = aGalleryExtension('a');
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery));
@@ -302,111 +311,6 @@ suite('ExtensionsActions', () => {
});
});
test('Test CombinedInstallAction when there is no extension', () => {
const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction);
instantiationService.createInstance(ExtensionContainers, [testObject]);
assert.ok(!testObject.enabled);
assert.equal('extension-action label prominent install no-extension', testObject.class);
});
test('Test CombinedInstallAction when extension is system extension', () => {
const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const local = aLocalExtension('a', {}, { type: ExtensionType.System });
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]);
return instantiationService.get(IExtensionsWorkbenchService).queryLocal()
.then(extensions => {
testObject.extension = extensions[0];
assert.ok(!testObject.enabled);
assert.equal('extension-action label prominent install no-extension', testObject.class);
});
});
test('Test CombinedInstallAction when installAction is enabled', () => {
const workbenchService = instantiationService.get(IExtensionsWorkbenchService);
const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const gallery = aGalleryExtension('a');
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery));
return workbenchService.queryGallery(CancellationToken.None)
.then((paged) => {
testObject.extension = paged.firstPage[0];
assert.ok(testObject.enabled);
assert.equal('Install', testObject.label);
assert.equal('extension-action label prominent install', testObject.class);
});
});
test('Test CombinedInstallAction when unInstallAction is enabled', () => {
const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const local = aLocalExtension('a');
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]);
return instantiationService.get(IExtensionsWorkbenchService).queryLocal()
.then(extensions => {
testObject.extension = extensions[0];
assert.ok(testObject.enabled);
assert.equal('Uninstall', testObject.label);
assert.equal('extension-action label uninstall', testObject.class);
});
});
test('Test CombinedInstallAction when state is installing', () => {
const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const workbenchService = instantiationService.get(IExtensionsWorkbenchService);
const gallery = aGalleryExtension('a');
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery));
return workbenchService.queryGallery(CancellationToken.None)
.then((paged) => {
testObject.extension = paged.firstPage[0];
installEvent.fire({ identifier: gallery.identifier, gallery });
assert.ok(!testObject.enabled);
assert.equal('Installing', testObject.label);
assert.equal('extension-action label install installing', testObject.class);
});
});
test('Test CombinedInstallAction when state is installing during update', () => {
const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const local = aLocalExtension('a');
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]);
return instantiationService.get(IExtensionsWorkbenchService).queryLocal()
.then(extensions => {
const gallery = aGalleryExtension('a');
const extension = extensions[0];
extension.gallery = gallery;
testObject.extension = extension;
installEvent.fire({ identifier: gallery.identifier, gallery });
assert.ok(!testObject.enabled);
assert.equal('Installing', testObject.label);
assert.equal('extension-action label install installing', testObject.class);
});
});
test('Test CombinedInstallAction when state is uninstalling', () => {
const testObject: ExtensionsActions.CombinedInstallAction = instantiationService.createInstance(ExtensionsActions.CombinedInstallAction);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const local = aLocalExtension('a');
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]);
return instantiationService.get(IExtensionsWorkbenchService).queryLocal()
.then(extensions => {
testObject.extension = extensions[0];
uninstallEvent.fire(local.identifier);
assert.ok(!testObject.enabled);
assert.equal('Uninstalling', testObject.label);
assert.equal('extension-action label uninstall uninstalling', testObject.class);
});
});
test('Test UpdateAction when there is no extension', () => {
const testObject: ExtensionsActions.UpdateAction = instantiationService.createInstance(ExtensionsActions.UpdateAction);
instantiationService.createInstance(ExtensionContainers, [testObject]);
@@ -515,7 +419,7 @@ suite('ExtensionsActions', () => {
.then(extensions => {
testObject.extension = extensions[0];
assert.ok(testObject.enabled);
assert.equal('extension-action icon manage codicon-gear', testObject.class);
assert.equal('extension-action icon manage codicon codicon-extensions-manage', testObject.class);
assert.equal('', testObject.tooltip);
});
});
@@ -530,7 +434,7 @@ suite('ExtensionsActions', () => {
.then(page => {
testObject.extension = page.firstPage[0];
assert.ok(!testObject.enabled);
assert.equal('extension-action icon manage codicon-gear hide', testObject.class);
assert.equal('extension-action icon manage codicon codicon-extensions-manage hide', testObject.class);
assert.equal('', testObject.tooltip);
});
});
@@ -547,7 +451,7 @@ suite('ExtensionsActions', () => {
installEvent.fire({ identifier: gallery.identifier, gallery });
assert.ok(!testObject.enabled);
assert.equal('extension-action icon manage codicon-gear hide', testObject.class);
assert.equal('extension-action icon manage codicon codicon-extensions-manage hide', testObject.class);
assert.equal('', testObject.tooltip);
});
});
@@ -565,7 +469,7 @@ suite('ExtensionsActions', () => {
didInstallEvent.fire({ identifier: gallery.identifier, gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) });
assert.ok(testObject.enabled);
assert.equal('extension-action icon manage codicon-gear', testObject.class);
assert.equal('extension-action icon manage codicon codicon-extensions-manage', testObject.class);
assert.equal('', testObject.tooltip);
});
});
@@ -580,7 +484,7 @@ suite('ExtensionsActions', () => {
.then(extensions => {
testObject.extension = extensions[0];
assert.ok(testObject.enabled);
assert.equal('extension-action icon manage codicon-gear', testObject.class);
assert.equal('extension-action icon manage codicon codicon-extensions-manage', testObject.class);
assert.equal('', testObject.tooltip);
});
});
@@ -597,7 +501,7 @@ suite('ExtensionsActions', () => {
uninstallEvent.fire(local.identifier);
assert.ok(!testObject.enabled);
assert.equal('extension-action icon manage codicon-gear', testObject.class);
assert.equal('extension-action icon manage codicon codicon-extensions-manage', testObject.class);
assert.equal('Uninstalling', testObject.tooltip);
});
});
@@ -914,25 +818,19 @@ suite('ExtensionsActions', () => {
});
});
test('Test DisableDropDownAction when there is no extension', () => {
const testObject: ExtensionsActions.DisableDropDownAction = instantiationService.createInstance(ExtensionsActions.DisableDropDownAction, []);
assert.ok(!testObject.enabled);
});
test('Test DisableDropDownAction when extension is installed and enabled', () => {
test('Test DisableGloballyAction when extension is installed and enabled', () => {
const local = aLocalExtension('a');
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]);
return instantiationService.get(IExtensionsWorkbenchService).queryLocal()
.then(extensions => {
const testObject: ExtensionsActions.DisableDropDownAction = instantiationService.createInstance(ExtensionsActions.DisableDropDownAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]);
const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]);
testObject.extension = extensions[0];
assert.ok(testObject.enabled);
});
});
test('Test DisableDropDownAction when extension is installed and disabled globally', () => {
test('Test DisableGloballyAction when extension is installed and disabled globally', () => {
const local = aLocalExtension('a');
return instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally)
.then(() => {
@@ -940,47 +838,32 @@ suite('ExtensionsActions', () => {
return instantiationService.get(IExtensionsWorkbenchService).queryLocal()
.then(extensions => {
const testObject: ExtensionsActions.DisableDropDownAction = instantiationService.createInstance(ExtensionsActions.DisableDropDownAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]);
const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]);
testObject.extension = extensions[0];
assert.ok(!testObject.enabled);
});
});
});
test('Test DisableDropDownAction when extension is installed and disabled for workspace', () => {
const local = aLocalExtension('a');
return instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledWorkspace)
.then(() => {
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]);
return instantiationService.get(IExtensionsWorkbenchService).queryLocal()
.then(extensions => {
const testObject: ExtensionsActions.DisableDropDownAction = instantiationService.createInstance(ExtensionsActions.DisableDropDownAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]);
testObject.extension = extensions[0];
assert.ok(!testObject.enabled);
});
});
});
test('Test DisableDropDownAction when extension is uninstalled', () => {
test('Test DisableGloballyAction when extension is uninstalled', () => {
const gallery = aGalleryExtension('a');
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery));
return instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None)
.then(page => {
const testObject: ExtensionsActions.DisableDropDownAction = instantiationService.createInstance(ExtensionsActions.DisableDropDownAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]);
const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]);
testObject.extension = page.firstPage[0];
assert.ok(!testObject.enabled);
});
});
test('Test DisableDropDownAction when extension is installing', () => {
test('Test DisableGloballyAction when extension is installing', () => {
const gallery = aGalleryExtension('a');
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery));
return instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None)
.then(page => {
const testObject: ExtensionsActions.DisableDropDownAction = instantiationService.createInstance(ExtensionsActions.DisableDropDownAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]);
const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]);
testObject.extension = page.firstPage[0];
instantiationService.createInstance(ExtensionContainers, [testObject]);
installEvent.fire({ identifier: gallery.identifier, gallery });
@@ -988,13 +871,13 @@ suite('ExtensionsActions', () => {
});
});
test('Test DisableDropDownAction when extension is uninstalling', () => {
test('Test DisableGloballyAction when extension is uninstalling', () => {
const local = aLocalExtension('a');
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]);
return instantiationService.get(IExtensionsWorkbenchService).queryLocal()
.then(extensions => {
const testObject: ExtensionsActions.DisableDropDownAction = instantiationService.createInstance(ExtensionsActions.DisableDropDownAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]);
const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]);
testObject.extension = extensions[0];
instantiationService.createInstance(ExtensionContainers, [testObject]);
uninstallEvent.fire(local.identifier);
@@ -1003,20 +886,20 @@ suite('ExtensionsActions', () => {
});
test('Test UpdateAllAction when no installed extensions', () => {
const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label');
const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label', true);
assert.ok(!testObject.enabled);
});
test('Test UpdateAllAction when installed extensions are not outdated', () => {
const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label');
const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label', true);
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [aLocalExtension('a'), aLocalExtension('b')]);
return instantiationService.get(IExtensionsWorkbenchService).queryLocal()
.then(extensions => assert.ok(!testObject.enabled));
});
test('Test UpdateAllAction when some installed extensions are outdated', () => {
const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label');
const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label', true);
const local = [aLocalExtension('a', { version: '1.0.1' }), aLocalExtension('b', { version: '1.0.1' }), aLocalExtension('c', { version: '1.0.1' })];
const workbenchService = instantiationService.get(IExtensionsWorkbenchService);
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', local);
@@ -1036,7 +919,7 @@ suite('ExtensionsActions', () => {
});
test('Test UpdateAllAction when some installed extensions are outdated and some outdated are being installed', () => {
const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label');
const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label', true);
const local = [aLocalExtension('a', { version: '1.0.1' }), aLocalExtension('b', { version: '1.0.1' }), aLocalExtension('c', { version: '1.0.1' })];
const gallery = [aGalleryExtension('a', { identifier: local[0].identifier, version: '1.0.2' }), aGalleryExtension('b', { identifier: local[1].identifier, version: '1.0.2' }), aGalleryExtension('c', local[2].manifest)];
const workbenchService = instantiationService.get(IExtensionsWorkbenchService);
@@ -1058,7 +941,7 @@ suite('ExtensionsActions', () => {
});
test('Test UpdateAllAction when some installed extensions are outdated and all outdated are being installed', () => {
const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label');
const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label', true);
const local = [aLocalExtension('a', { version: '1.0.1' }), aLocalExtension('b', { version: '1.0.1' }), aLocalExtension('c', { version: '1.0.1' })];
const gallery = [aGalleryExtension('a', { identifier: local[0].identifier, version: '1.0.2' }), aGalleryExtension('b', { identifier: local[1].identifier, version: '1.0.2' }), aGalleryExtension('c', local[2].manifest)];
const workbenchService = instantiationService.get(IExtensionsWorkbenchService);
@@ -1120,7 +1003,7 @@ suite('ReloadAction', () => {
test('Test ReloadAction when extension is newly installed', async () => {
const onDidChangeExtensionsEmitter: Emitter<void> = new Emitter<void>();
const runningExtensions = [ExtensionsActions.toExtensionDescription(aLocalExtension('b'))];
const runningExtensions = [toExtensionDescription(aLocalExtension('b'))];
instantiationService.stub(IExtensionService, <Partial<IExtensionService>>{
getExtensions: () => Promise.resolve(runningExtensions),
onDidChangeExtensions: onDidChangeExtensionsEmitter.event,
@@ -1143,7 +1026,7 @@ suite('ReloadAction', () => {
test('Test ReloadAction when extension is newly installed and reload is not required', async () => {
const onDidChangeExtensionsEmitter: Emitter<void> = new Emitter<void>();
const runningExtensions = [ExtensionsActions.toExtensionDescription(aLocalExtension('b'))];
const runningExtensions = [toExtensionDescription(aLocalExtension('b'))];
instantiationService.stub(IExtensionService, <Partial<IExtensionService>>{
getExtensions: () => Promise.resolve(runningExtensions),
onDidChangeExtensions: onDidChangeExtensionsEmitter.event,
@@ -1164,7 +1047,7 @@ suite('ReloadAction', () => {
});
test('Test ReloadAction when extension is installed and uninstalled', async () => {
instantiationService.stubPromise(IExtensionService, 'getExtensions', [ExtensionsActions.toExtensionDescription(aLocalExtension('b'))]);
instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('b'))]);
const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const gallery = aGalleryExtension('a');
@@ -1182,7 +1065,7 @@ suite('ReloadAction', () => {
});
test('Test ReloadAction when extension is uninstalled', async () => {
instantiationService.stubPromise(IExtensionService, 'getExtensions', [ExtensionsActions.toExtensionDescription(aLocalExtension('a', { version: '1.0.0' }))]);
instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('a', { version: '1.0.0' }))]);
const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const local = aLocalExtension('a');
@@ -1199,7 +1082,7 @@ suite('ReloadAction', () => {
test('Test ReloadAction when extension is uninstalled and can be removed', async () => {
const local = aLocalExtension('a');
instantiationService.stub(IExtensionService, <Partial<IExtensionService>>{
getExtensions: () => Promise.resolve([ExtensionsActions.toExtensionDescription(local)]),
getExtensions: () => Promise.resolve([toExtensionDescription(local)]),
onDidChangeExtensions: new Emitter<void>().event,
canRemoveExtension: (extension) => true,
canAddExtension: (extension) => true
@@ -1216,7 +1099,7 @@ suite('ReloadAction', () => {
});
test('Test ReloadAction when extension is uninstalled and installed', async () => {
instantiationService.stubPromise(IExtensionService, 'getExtensions', [ExtensionsActions.toExtensionDescription(aLocalExtension('a', { version: '1.0.0' }))]);
instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('a', { version: '1.0.0' }))]);
const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const local = aLocalExtension('a');
@@ -1236,7 +1119,7 @@ suite('ReloadAction', () => {
});
test('Test ReloadAction when extension is updated while running', async () => {
instantiationService.stubPromise(IExtensionService, 'getExtensions', [ExtensionsActions.toExtensionDescription(aLocalExtension('a', { version: '1.0.1' }))]);
instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('a', { version: '1.0.1' }))]);
const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const local = aLocalExtension('a', { version: '1.0.1' });
@@ -1258,7 +1141,7 @@ suite('ReloadAction', () => {
});
test('Test ReloadAction when extension is updated when not running', async () => {
instantiationService.stubPromise(IExtensionService, 'getExtensions', [ExtensionsActions.toExtensionDescription(aLocalExtension('b'))]);
instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('b'))]);
const local = aLocalExtension('a', { version: '1.0.1' });
await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally);
const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction);
@@ -1276,7 +1159,7 @@ suite('ReloadAction', () => {
});
test('Test ReloadAction when extension is disabled when running', async () => {
instantiationService.stubPromise(IExtensionService, 'getExtensions', [ExtensionsActions.toExtensionDescription(aLocalExtension('a'))]);
instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('a'))]);
const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const local = aLocalExtension('a');
@@ -1292,7 +1175,7 @@ suite('ReloadAction', () => {
});
test('Test ReloadAction when extension enablement is toggled when running', async () => {
instantiationService.stubPromise(IExtensionService, 'getExtensions', [ExtensionsActions.toExtensionDescription(aLocalExtension('a', { version: '1.0.0' }))]);
instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('a', { version: '1.0.0' }))]);
const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const local = aLocalExtension('a');
@@ -1306,7 +1189,7 @@ suite('ReloadAction', () => {
});
test('Test ReloadAction when extension is enabled when not running', async () => {
instantiationService.stubPromise(IExtensionService, 'getExtensions', [ExtensionsActions.toExtensionDescription(aLocalExtension('b'))]);
instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('b'))]);
const local = aLocalExtension('a');
await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally);
const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction);
@@ -1322,7 +1205,7 @@ suite('ReloadAction', () => {
});
test('Test ReloadAction when extension enablement is toggled when not running', async () => {
instantiationService.stubPromise(IExtensionService, 'getExtensions', [ExtensionsActions.toExtensionDescription(aLocalExtension('b'))]);
instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('b'))]);
const local = aLocalExtension('a');
await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally);
const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction);
@@ -1337,7 +1220,7 @@ suite('ReloadAction', () => {
});
test('Test ReloadAction when extension is updated when not running and enabled', async () => {
instantiationService.stubPromise(IExtensionService, 'getExtensions', [ExtensionsActions.toExtensionDescription(aLocalExtension('b'))]);
instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('b'))]);
const local = aLocalExtension('a', { version: '1.0.1' });
await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally);
const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction);
@@ -1357,7 +1240,7 @@ suite('ReloadAction', () => {
});
test('Test ReloadAction when a localization extension is newly installed', async () => {
instantiationService.stubPromise(IExtensionService, 'getExtensions', [ExtensionsActions.toExtensionDescription(aLocalExtension('b'))]);
instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('b'))]);
const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const gallery = aGalleryExtension('a');
@@ -1373,7 +1256,7 @@ suite('ReloadAction', () => {
});
test('Test ReloadAction when a localization extension is updated while running', async () => {
instantiationService.stubPromise(IExtensionService, 'getExtensions', [ExtensionsActions.toExtensionDescription(aLocalExtension('a', { version: '1.0.1' }))]);
instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('a', { version: '1.0.1' }))]);
const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const local = aLocalExtension('a', { version: '1.0.1', contributes: <IExtensionContributions>{ localizations: [{ languageId: 'de', translations: [] }] } });
@@ -1400,7 +1283,7 @@ suite('ReloadAction', () => {
instantiationService.set(IExtensionsWorkbenchService, workbenchService);
const onDidChangeExtensionsEmitter: Emitter<void> = new Emitter<void>();
const runningExtensions = [ExtensionsActions.toExtensionDescription(remoteExtension)];
const runningExtensions = [toExtensionDescription(remoteExtension)];
instantiationService.stub(IExtensionService, <Partial<IExtensionService>>{
getExtensions: () => Promise.resolve(runningExtensions),
onDidChangeExtensions: onDidChangeExtensionsEmitter.event,
@@ -1434,7 +1317,7 @@ suite('ReloadAction', () => {
instantiationService.set(IExtensionsWorkbenchService, workbenchService);
const onDidChangeExtensionsEmitter: Emitter<void> = new Emitter<void>();
const runningExtensions = [ExtensionsActions.toExtensionDescription(remoteExtension)];
const runningExtensions = [toExtensionDescription(remoteExtension)];
instantiationService.stub(IExtensionService, <Partial<IExtensionService>>{
getExtensions: () => Promise.resolve(runningExtensions),
onDidChangeExtensions: onDidChangeExtensionsEmitter.event,
@@ -1544,7 +1427,7 @@ suite('ReloadAction', () => {
const onDidChangeExtensionsEmitter: Emitter<void> = new Emitter<void>();
instantiationService.stub(IExtensionService, <Partial<IExtensionService>>{
getExtensions: () => Promise.resolve([ExtensionsActions.toExtensionDescription(localExtension)]),
getExtensions: () => Promise.resolve([toExtensionDescription(localExtension)]),
onDidChangeExtensions: onDidChangeExtensionsEmitter.event,
canAddExtension: (extension) => false
});
@@ -1575,7 +1458,7 @@ suite('ReloadAction', () => {
const onDidChangeExtensionsEmitter: Emitter<void> = new Emitter<void>();
instantiationService.stub(IExtensionService, <Partial<IExtensionService>>{
getExtensions: () => Promise.resolve([ExtensionsActions.toExtensionDescription(localExtension)]),
getExtensions: () => Promise.resolve([toExtensionDescription(localExtension)]),
onDidChangeExtensions: onDidChangeExtensionsEmitter.event,
canAddExtension: (extension) => false
});
@@ -1606,7 +1489,7 @@ suite('ReloadAction', () => {
const onDidChangeExtensionsEmitter: Emitter<void> = new Emitter<void>();
instantiationService.stub(IExtensionService, <Partial<IExtensionService>>{
getExtensions: () => Promise.resolve([ExtensionsActions.toExtensionDescription(remoteExtension)]),
getExtensions: () => Promise.resolve([toExtensionDescription(remoteExtension)]),
onDidChangeExtensions: onDidChangeExtensionsEmitter.event,
canAddExtension: (extension) => false
});
@@ -1637,7 +1520,7 @@ suite('ReloadAction', () => {
const onDidChangeExtensionsEmitter: Emitter<void> = new Emitter<void>();
instantiationService.stub(IExtensionService, <Partial<IExtensionService>>{
getExtensions: () => Promise.resolve([ExtensionsActions.toExtensionDescription(localExtension)]),
getExtensions: () => Promise.resolve([toExtensionDescription(localExtension)]),
onDidChangeExtensions: onDidChangeExtensionsEmitter.event,
canAddExtension: (extension) => false
});
@@ -1668,7 +1551,7 @@ suite('ReloadAction', () => {
const onDidChangeExtensionsEmitter: Emitter<void> = new Emitter<void>();
instantiationService.stub(IExtensionService, <Partial<IExtensionService>>{
getExtensions: () => Promise.resolve([ExtensionsActions.toExtensionDescription(remoteExtension)]),
getExtensions: () => Promise.resolve([toExtensionDescription(remoteExtension)]),
onDidChangeExtensions: onDidChangeExtensionsEmitter.event,
canAddExtension: (extension) => false
});
@@ -1904,6 +1787,7 @@ suite('RemoteInstallAction', () => {
const localWorkspaceExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`) });
const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localWorkspaceExtension]));
instantiationService.stub(IWorkbenchEnvironmentService, { disableExtensions: true } as IWorkbenchEnvironmentService);
instantiationService.stub(INativeWorkbenchEnvironmentService, { disableExtensions: true } as INativeWorkbenchEnvironmentService);
instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService);
instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService));
const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService);
@@ -2283,6 +2167,7 @@ suite('LocalInstallAction', () => {
// multi server setup
const remoteUIExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) });
instantiationService.stub(IWorkbenchEnvironmentService, { disableExtensions: true } as IWorkbenchEnvironmentService);
instantiationService.stub(INativeWorkbenchEnvironmentService, { disableExtensions: true } as INativeWorkbenchEnvironmentService);
const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService(), createExtensionManagementService([remoteUIExtension]));
instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService);
instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService));
@@ -2499,6 +2384,7 @@ function aLocalExtension(name: string = 'someext', manifest: any = {}, propertie
identifier: { id: getGalleryExtensionId(manifest.publisher, manifest.name) },
...properties
};
properties.isBuiltin = properties.type === ExtensionType.System;
return <ILocalExtension>Object.create({ manifest, ...properties });
}

View File

@@ -13,16 +13,17 @@ import {
IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions,
DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, SortBy
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IExtensionRecommendationsService, ExtensionRecommendationReason, IExtensionRecommendation } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionRecommendationsService, ExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test';
import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
import { IURLService } from 'vs/platform/url/common/url';
import { Emitter } from 'vs/base/common/event';
import { Emitter, Event } from 'vs/base/common/event';
import { IPager } from 'vs/base/common/paging';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IExtensionService, toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { TestMenuService } from 'vs/workbench/test/browser/workbenchTestServices';
import { TestSharedProcessService } from 'vs/workbench/test/electron-browser/workbenchTestServices';
@@ -35,16 +36,14 @@ import { SinonStub } from 'sinon';
import { IExperimentService, ExperimentState, ExperimentActionType, ExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl';
import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ExtensionType, IExtension, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
import { ExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService';
import { ILabelService } from 'vs/platform/label/common/label';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
import { IMenuService } from 'vs/platform/actions/common/actions';
import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices';
import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views';
import { IProductService } from 'vs/platform/product/common/productService';
import { Schemas } from 'vs/base/common/network';
suite('ExtensionsListView Tests', () => {
@@ -101,14 +100,18 @@ suite('ExtensionsListView Tests', () => {
instantiationService.stub(IContextKeyService, new MockContextKeyService());
instantiationService.stub(IMenuService, new TestMenuService());
instantiationService.stub(IExtensionManagementServerService, new class extends ExtensionManagementServerService {
#localExtensionManagementServer: IExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local', id: 'vscode-local' };
constructor() {
super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService), instantiationService.get(ILabelService), instantiationService.get(IExtensionGalleryService), instantiationService.get(IProductService), instantiationService.get(IConfigurationService), instantiationService.get(ILogService));
const localExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local', id: 'vscode-local' };
instantiationService.stub(IExtensionManagementServerService, <Partial<IExtensionManagementServerService>>{
get localExtensionManagementServer(): IExtensionManagementServer {
return localExtensionManagementServer;
},
getExtensionManagementServer(extension: IExtension): IExtensionManagementServer | null {
if (extension.location.scheme === Schemas.file) {
return localExtensionManagementServer;
}
throw new Error(`Invalid Extension ${extension.location}`);
}
get localExtensionManagementServer(): IExtensionManagementServer { return this.#localExtensionManagementServer; }
set localExtensionManagementServer(server: IExtensionManagementServer) { }
}());
});
instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService));
@@ -122,28 +125,28 @@ suite('ExtensionsListView Tests', () => {
instantiationService.stub(IExtensionRecommendationsService, <Partial<IExtensionRecommendationsService>>{
getWorkspaceRecommendations() {
return Promise.resolve([
{ extensionId: workspaceRecommendationA.identifier.id },
{ extensionId: workspaceRecommendationB.identifier.id }]);
workspaceRecommendationA.identifier.id,
workspaceRecommendationB.identifier.id]);
},
getConfigBasedRecommendations() {
return Promise.resolve({
important: [{ extensionId: configBasedRecommendationA.identifier.id }],
others: [{ extensionId: configBasedRecommendationB.identifier.id }],
important: [configBasedRecommendationA.identifier.id],
others: [configBasedRecommendationB.identifier.id],
});
},
getImportantRecommendations(): Promise<IExtensionRecommendation[]> {
getImportantRecommendations(): Promise<string[]> {
return Promise.resolve([]);
},
getFileBasedRecommendations() {
return [
{ extensionId: fileBasedRecommendationA.identifier.id },
{ extensionId: fileBasedRecommendationB.identifier.id }
fileBasedRecommendationA.identifier.id,
fileBasedRecommendationB.identifier.id
];
},
getOtherRecommendations() {
return Promise.resolve([
{ extensionId: configBasedRecommendationB.identifier.id },
{ extensionId: otherRecommendationA.identifier.id }
configBasedRecommendationB.identifier.id,
otherRecommendationA.identifier.id
]);
},
getAllRecommendationsWithReason() {
@@ -165,7 +168,8 @@ suite('ExtensionsListView Tests', () => {
}
});
instantiationService.stub(IExtensionService, {
instantiationService.stub(IExtensionService, <Partial<IExtensionService>>{
onDidChangeExtensions: Event.None,
getExtensions: (): Promise<IExtensionDescription[]> => {
return Promise.resolve([
toExtensionDescription(localEnabledTheme),
@@ -180,7 +184,7 @@ suite('ExtensionsListView Tests', () => {
await (<TestExtensionEnablementService>instantiationService.get(IWorkbenchExtensionEnablementService)).setEnablement([localDisabledLanguage], EnablementState.DisabledGlobally);
instantiationService.set(IExtensionsWorkbenchService, instantiationService.createInstance(ExtensionsWorkbenchService));
testableView = instantiationService.createInstance(ExtensionsListView, {});
testableView = instantiationService.createInstance(ExtensionsListView, {}, {});
});
teardown(() => {
@@ -482,7 +486,7 @@ suite('ExtensionsListView Tests', () => {
}]);
testableView.dispose();
testableView = instantiationService.createInstance(ExtensionsListView, {});
testableView = instantiationService.createInstance(ExtensionsListView, {}, {});
return testableView.show('search-me').then(result => {
const options: IQueryOptions = queryTarget.args[0][0];
@@ -509,7 +513,7 @@ suite('ExtensionsListView Tests', () => {
const queryTarget = <SinonStub>instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...realResults));
testableView.dispose();
testableView = instantiationService.createInstance(ExtensionsListView, {});
testableView = instantiationService.createInstance(ExtensionsListView, {}, {});
return testableView.show('search-me @sort:installs').then(result => {
const options: IQueryOptions = queryTarget.args[0][0];
@@ -532,6 +536,7 @@ suite('ExtensionsListView Tests', () => {
metadata: { id: getGalleryExtensionId(manifest.publisher, manifest.name), publisherId: manifest.publisher, publisherDisplayName: 'somename' },
...properties
};
properties.isBuiltin = properties.type === ExtensionType.System;
return <ILocalExtension>Object.create({ manifest, ...properties });
}
@@ -547,14 +552,5 @@ suite('ExtensionsListView Tests', () => {
return { firstPage: objects, total: objects.length, pageSize: objects.length, getPage: () => null! };
}
function toExtensionDescription(local: ILocalExtension): IExtensionDescription {
return {
identifier: new ExtensionIdentifier(local.identifier.id),
isBuiltin: local.type === ExtensionType.System,
isUnderDevelopment: false,
extensionLocation: local.location,
...local.manifest
};
}
});

View File

@@ -13,7 +13,8 @@ import {
IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension,
DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IGalleryExtensionAssets, IExtensionIdentifier, InstallOperation, IExtensionTipsService, IGalleryMetadata
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionRecommendationsService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test';
import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
@@ -38,13 +39,12 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA
import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl';
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices';
import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { IProductService } from 'vs/platform/product/common/productService';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices';
import { IExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService';
import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-browser/experimentService.test';
import { ExtensionTipsService } from 'vs/platform/extensionManagement/node/extensionTipsService';
import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-sandbox/extensionTipsService';
import { Schemas } from 'vs/base/common/network';
suite('ExtensionsWorkbenchServiceTest', () => {
@@ -67,7 +67,6 @@ suite('ExtensionsWorkbenchServiceTest', () => {
instantiationService.stub(ITelemetryService, NullTelemetryService);
instantiationService.stub(ILogService, NullLogService);
instantiationService.stub(IProgressService, ProgressService);
instantiationService.stub(IStorageKeysSyncRegistryService, new StorageKeysSyncRegistryService());
instantiationService.stub(IProductService, {});
instantiationService.stub(IExtensionGalleryService, ExtensionGalleryService);
@@ -223,8 +222,8 @@ suite('ExtensionsWorkbenchServiceTest', () => {
assert.equal('1.1.0', actual.version);
assert.equal('1.1.0', actual.latestVersion);
assert.equal('localDescription1', actual.description);
assert.equal('file:///localPath1/localIcon1', actual.iconUrl);
assert.equal('file:///localPath1/localIcon1', actual.iconUrlFallback);
assert.ok(actual.iconUrl === 'file:///localPath1/localIcon1' || actual.iconUrl === 'vscode-file://vscode-app/localPath1/localIcon1');
assert.ok(actual.iconUrlFallback === 'file:///localPath1/localIcon1' || actual.iconUrlFallback === 'vscode-file://vscode-app/localPath1/localIcon1');
assert.equal(null, actual.licenseUrl);
assert.equal(ExtensionState.Installed, actual.state);
assert.equal(null, actual.installCount);
@@ -686,6 +685,11 @@ suite('ExtensionsWorkbenchServiceTest', () => {
const extensionB = aLocalExtension('b');
const extensionC = aLocalExtension('c');
instantiationService.stub(INotificationService, <Partial<INotificationService>>{
prompt(severity, message, choices, options) {
options!.onCancel!();
}
});
return instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([extensionA], EnablementState.EnabledGlobally)
.then(() => instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([extensionB], EnablementState.EnabledGlobally))
.then(() => instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([extensionC], EnablementState.EnabledGlobally))
@@ -696,6 +700,28 @@ suite('ExtensionsWorkbenchServiceTest', () => {
});
});
test('test disable extension disables all dependents when chosen to disable all', async () => {
const extensionA = aLocalExtension('a', { extensionDependencies: ['pub.b'] });
const extensionB = aLocalExtension('b');
const extensionC = aLocalExtension('c');
instantiationService.stub(INotificationService, <Partial<INotificationService>>{
prompt(severity, message, choices, options) {
choices[0].run();
}
});
return instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([extensionA], EnablementState.EnabledGlobally)
.then(() => instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([extensionB], EnablementState.EnabledGlobally))
.then(() => instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([extensionC], EnablementState.EnabledGlobally))
.then(async () => {
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [extensionA, extensionB, extensionC]);
testObject = await aWorkbenchService();
await testObject.setEnablement(testObject.local[1], EnablementState.DisabledGlobally);
assert.equal(testObject.local[0].enablementState, EnablementState.DisabledGlobally);
assert.equal(testObject.local[1].enablementState, EnablementState.DisabledGlobally);
});
});
test('test disable extension when extension is part of a pack', async () => {
const extensionA = aLocalExtension('a', { extensionPack: ['pub.b'] });
const extensionB = aLocalExtension('b');
@@ -801,6 +827,11 @@ suite('ExtensionsWorkbenchServiceTest', () => {
const extensionB = aLocalExtension('b', { extensionDependencies: ['pub.a'] });
const extensionC = aLocalExtension('c');
instantiationService.stub(INotificationService, <Partial<INotificationService>>{
prompt(severity, message, choices, options) {
options!.onCancel!();
}
});
return instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([extensionA], EnablementState.EnabledGlobally)
.then(() => instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([extensionB], EnablementState.EnabledGlobally))
.then(() => instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([extensionC], EnablementState.EnabledGlobally))
@@ -836,6 +867,11 @@ suite('ExtensionsWorkbenchServiceTest', () => {
const extensionB = aLocalExtension('b', { extensionDependencies: ['pub.c'] });
const extensionC = aLocalExtension('c', { extensionDependencies: ['pub.a'] });
instantiationService.stub(INotificationService, <Partial<INotificationService>>{
prompt(severity, message, choices, options) {
options!.onCancel!();
}
});
return instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([extensionA], EnablementState.EnabledGlobally)
.then(() => instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([extensionB], EnablementState.EnabledGlobally))
.then(() => instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([extensionC], EnablementState.EnabledGlobally))