Revert "Adds support for stronglyRecommended extensions. Implements #299039"

This reverts commit 240196b595.
This commit is contained in:
Henning Dieterichs
2026-03-04 18:44:45 +01:00
parent a19a6969a2
commit 4ab6fddf3e
13 changed files with 3 additions and 455 deletions

View File

@@ -4,15 +4,11 @@
"recommendations": [
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"github.vscode-pull-request-github",
"ms-vscode.vscode-github-issue-notebooks",
"ms-vscode.extension-test-runner",
"jrieken.vscode-pr-pinger",
"typescriptteam.native-preview",
"ms-vscode.ts-customized-language-service"
],
"stronglyRecommended": [
"github.vscode-pull-request-github",
"ms-vscode.vscode-extras",
"ms-vscode.vscode-selfhost-test-provider"
]
}

View File

@@ -5,7 +5,6 @@
import { CancellationToken } from '../../../base/common/cancellation.js';
import { Event } from '../../../base/common/event.js';
import { DisposableStore } from '../../../base/common/lifecycle.js';
import { ThemeIcon } from '../../../base/common/themables.js';
import { IMarkdownString } from '../../../base/common/htmlContent.js';
import { basename } from '../../../base/common/resources.js';
@@ -287,23 +286,12 @@ export const IDialogService = createDecorator<IDialogService>('dialogService');
export interface ICustomDialogOptions {
readonly buttonDetails?: string[];
readonly buttonOptions?: Array<undefined | ICustomDialogButtonOptions>;
readonly markdownDetails?: ICustomDialogMarkdown[];
readonly renderBody?: (container: HTMLElement, disposables: DisposableStore) => void;
readonly classes?: string[];
readonly icon?: ThemeIcon;
readonly disableCloseAction?: boolean;
}
export interface ICustomDialogButtonOptions {
readonly sublabel?: string;
readonly styleButton?: (button: ICustomDialogButtonControl) => void;
}
export interface ICustomDialogButtonControl {
set enabled(value: boolean);
}
export interface ICustomDialogMarkdown {
readonly markdown: IMarkdownString;
readonly classes?: string[];

View File

@@ -45,7 +45,5 @@ export interface IExtensionRecommendationNotificationService {
promptImportantExtensionsInstallNotification(recommendations: IExtensionRecommendations): Promise<RecommendationsNotificationResult>;
promptWorkspaceRecommendations(recommendations: Array<string | URI>): Promise<void>;
promptStronglyRecommendedExtensions(recommendations: Array<string | URI>): Promise<void>;
resetStronglyRecommendedIgnoreState(): void;
}

View File

@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import { Event } from '../../../base/common/event.js';
import { URI } from '../../../base/common/uri.js';
import { IChannel, IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
import { IExtensionRecommendationNotificationService, IExtensionRecommendations, RecommendationsNotificationResult } from './extensionRecommendations.js';
@@ -24,18 +23,10 @@ export class ExtensionRecommendationNotificationServiceChannelClient implements
throw new Error('not supported');
}
promptStronglyRecommendedExtensions(recommendations: Array<string | URI>): Promise<void> {
throw new Error('not supported');
}
hasToIgnoreRecommendationNotifications(): boolean {
throw new Error('not supported');
}
resetStronglyRecommendedIgnoreState(): void {
throw new Error('not supported');
}
}
export class ExtensionRecommendationNotificationServiceChannel implements IServerChannel {

View File

@@ -9,7 +9,6 @@ import { IConfirmation, IConfirmationResult, IInputResult, ICheckbox, IInputElem
import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import Severity from '../../../../base/common/severity.js';
import { IButton } from '../../../../base/browser/ui/button/button.js';
import { Dialog, IDialogResult } from '../../../../base/browser/ui/dialog/dialog.js';
import { DisposableStore } from '../../../../base/common/lifecycle.js';
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
@@ -106,14 +105,8 @@ export class BrowserDialogHandler extends AbstractDialogHandler {
parent.appendChild(result.element);
result.element.classList.add(...(markdownDetail.classes || []));
});
customOptions.renderBody?.(parent, dialogDisposables);
} : undefined;
const buttonOptions = customOptions?.buttonOptions?.map(opt => opt ? {
sublabel: opt.sublabel,
styleButton: opt.styleButton ? (button: IButton) => opt.styleButton!(button) : undefined
} : undefined) ?? customOptions?.buttonDetails?.map(detail => ({ sublabel: detail }));
const dialog = new Dialog(
this.layoutService.activeContainer,
message,
@@ -125,7 +118,7 @@ export class BrowserDialogHandler extends AbstractDialogHandler {
renderBody,
icon: customOptions?.icon,
disableCloseAction: customOptions?.disableCloseAction,
buttonOptions,
buttonOptions: customOptions?.buttonDetails?.map(detail => ({ sublabel: detail })),
checkboxLabel: checkbox?.label,
checkboxChecked: checkbox?.checked,
inputs

View File

@@ -6,7 +6,6 @@
import { distinct } from '../../../../base/common/arrays.js';
import { CancelablePromise, createCancelablePromise, Promises, raceCancellablePromises, raceCancellation, timeout } from '../../../../base/common/async.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { isCancellationError } from '../../../../base/common/errors.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
@@ -14,13 +13,11 @@ import { isString } from '../../../../base/common/types.js';
import { URI } from '../../../../base/common/uri.js';
import { localize } from '../../../../nls.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { IGalleryExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js';
import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
import { IExtensionRecommendationNotificationService, IExtensionRecommendations, RecommendationsNotificationResult, RecommendationSource, RecommendationSourceToString } from '../../../../platform/extensionRecommendations/common/extensionRecommendations.js';
import { INotificationHandle, INotificationService, IPromptChoice, IPromptChoiceWithMenu, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { renderStronglyRecommendedExtensionList, StronglyRecommendedExtensionListResult } from './stronglyRecommendedExtensionList.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
import { IUserDataSyncEnablementService, SyncResource } from '../../../../platform/userDataSync/common/userDataSync.js';
@@ -45,18 +42,6 @@ type ExtensionWorkspaceRecommendationsNotificationClassification = {
const ignoreImportantExtensionRecommendationStorageKey = 'extensionsAssistant/importantRecommendationsIgnore';
const donotShowWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore';
const stronglyRecommendedIgnoreStorageKey = 'extensionsAssistant/stronglyRecommendedIgnore';
const stronglyRecommendedMajorVersionIgnoreStorageKey = 'extensionsAssistant/stronglyRecommendedMajorVersionIgnore';
interface MajorVersionIgnoreEntry {
readonly id: string;
readonly majorVersion: number;
}
function parseMajorVersion(version: string): number {
const major = parseInt(version.split('.')[0], 10);
return isNaN(major) ? 0 : major;
}
type RecommendationsNotificationActions = {
onDidInstallRecommendedExtensions(extensions: IExtension[]): void;
@@ -147,7 +132,6 @@ export class ExtensionRecommendationNotificationService extends Disposable imple
constructor(
@IConfigurationService private readonly configurationService: IConfigurationService,
@IDialogService private readonly dialogService: IDialogService,
@IStorageService private readonly storageService: IStorageService,
@INotificationService private readonly notificationService: INotificationService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@@ -484,159 +468,6 @@ export class ExtensionRecommendationNotificationService extends Disposable imple
}
}
async promptStronglyRecommendedExtensions(recommendations: Array<string | URI>): Promise<void> {
if (this.hasToIgnoreRecommendationNotifications()) {
return;
}
const ignoredList = this._getStronglyRecommendedIgnoreList();
recommendations = recommendations.filter(rec => {
const key = isString(rec) ? rec.toLowerCase() : rec.toString();
return !ignoredList.includes(key);
});
if (!recommendations.length) {
return;
}
let installed = await this.extensionManagementService.getInstalled();
installed = installed.filter(l => this.extensionEnablementService.getEnablementState(l) !== EnablementState.DisabledByExtensionKind);
recommendations = recommendations.filter(recommendation =>
installed.every(local =>
isString(recommendation) ? !areSameExtensions({ id: recommendation }, local.identifier) : !this.uriIdentityService.extUri.isEqual(recommendation, local.location)
)
);
if (!recommendations.length) {
return;
}
const allExtensions = await this.getInstallableExtensions(recommendations);
if (!allExtensions.length) {
return;
}
const majorVersionIgnoreList = this._getStronglyRecommendedMajorVersionIgnoreList();
const extensions = allExtensions.filter(ext => {
const ignored = majorVersionIgnoreList.find(e => e.id === ext.identifier.id.toLowerCase());
return !ignored || parseMajorVersion(ext.version) > ignored.majorVersion;
});
if (!extensions.length) {
return;
}
const message = extensions.length === 1
? localize('stronglyRecommended1', "This workspace strongly recommends installing the '{0}' extension. Do you want to install?", extensions[0].displayName)
: localize('stronglyRecommendedN', "This workspace strongly recommends installing {0} extensions. Do you want to install?", extensions.length);
let listResult!: StronglyRecommendedExtensionListResult<IExtension>;
const { result } = await this.dialogService.prompt<boolean | undefined>({
message,
buttons: [
{
label: localize('install', "Install"),
run: () => true,
},
],
cancelButton: localize('cancel', "Cancel"),
custom: {
icon: Codicon.extensions,
renderBody: (container, disposables) => {
listResult = renderStronglyRecommendedExtensionList(container, disposables, extensions);
},
buttonOptions: [{
styleButton: (button) => listResult.styleInstallButton(button),
}],
},
});
if (result) {
const selected = extensions.filter(e => listResult.checkboxStates.get(e));
const unselected = extensions.filter(e => !listResult.checkboxStates.get(e));
if (unselected.length) {
this._addToStronglyRecommendedIgnoreList(
unselected.map(e => e.identifier.id)
);
}
if (listResult.doNotShowAgainUnlessMajorVersionChange()) {
this._addToStronglyRecommendedIgnoreWithMajorVersion(
extensions.map(e => ({ id: e.identifier.id, majorVersion: parseMajorVersion(e.version) }))
);
}
if (selected.length) {
const galleryExtensions: IGalleryExtension[] = [];
const resourceExtensions: IExtension[] = [];
for (const extension of selected) {
if (extension.gallery) {
galleryExtensions.push(extension.gallery);
} else if (extension.resourceExtension) {
resourceExtensions.push(extension);
}
}
await Promises.settled<unknown>([
Promises.settled(selected.map(extension => this.extensionsWorkbenchService.open(extension, { pinned: true }))),
galleryExtensions.length ? this.extensionManagementService.installGalleryExtensions(galleryExtensions.map(e => ({ extension: e, options: {} }))) : Promise.resolve(),
resourceExtensions.length ? Promise.allSettled(resourceExtensions.map(r => this.extensionsWorkbenchService.install(r))) : Promise.resolve(),
]);
}
}
}
private _getStronglyRecommendedIgnoreList(): string[] {
const raw = this.storageService.get(stronglyRecommendedIgnoreStorageKey, StorageScope.WORKSPACE);
if (raw === undefined) {
return [];
}
try {
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
private _addToStronglyRecommendedIgnoreList(recommendations: Array<string | URI>): void {
const list = this._getStronglyRecommendedIgnoreList();
for (const rec of recommendations) {
const key = isString(rec) ? rec.toLowerCase() : rec.toString();
if (!list.includes(key)) {
list.push(key);
}
}
this.storageService.store(stronglyRecommendedIgnoreStorageKey, JSON.stringify(list), StorageScope.WORKSPACE, StorageTarget.MACHINE);
}
private _getStronglyRecommendedMajorVersionIgnoreList(): MajorVersionIgnoreEntry[] {
const raw = this.storageService.get(stronglyRecommendedMajorVersionIgnoreStorageKey, StorageScope.WORKSPACE);
if (raw === undefined) {
return [];
}
try {
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
private _addToStronglyRecommendedIgnoreWithMajorVersion(entries: MajorVersionIgnoreEntry[]): void {
const list = this._getStronglyRecommendedMajorVersionIgnoreList();
for (const entry of entries) {
const key = entry.id.toLowerCase();
const existing = list.findIndex(e => e.id === key);
if (existing !== -1) {
list[existing] = { id: key, majorVersion: entry.majorVersion };
} else {
list.push({ id: key, majorVersion: entry.majorVersion });
}
}
this.storageService.store(stronglyRecommendedMajorVersionIgnoreStorageKey, JSON.stringify(list), StorageScope.WORKSPACE, StorageTarget.MACHINE);
}
resetStronglyRecommendedIgnoreState(): void {
this.storageService.remove(stronglyRecommendedIgnoreStorageKey, StorageScope.WORKSPACE);
this.storageService.remove(stronglyRecommendedMajorVersionIgnoreStorageKey, StorageScope.WORKSPACE);
}
private setIgnoreRecommendationsConfig(configVal: boolean) {
this.configurationService.updateValue('extensions.ignoreRecommendations', configVal);
}

View File

@@ -110,7 +110,6 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations, this.extensionRecommendationsManagementService.onDidChangeIgnoredRecommendations)(() => this._onDidChangeRecommendations.fire()));
this.promptWorkspaceRecommendations();
this.promptStronglyRecommendedExtensions();
}
private isEnabled(): boolean {
@@ -275,15 +274,6 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
}
}
private async promptStronglyRecommendedExtensions(): Promise<void> {
const allowedRecommendations = this.workspaceRecommendations.stronglyRecommended
.filter(rec => !isString(rec) || this.isExtensionAllowedToBeRecommended(rec));
if (allowedRecommendations.length) {
await this.extensionRecommendationNotificationService.promptStronglyRecommendedExtensions(allowedRecommendations);
}
}
private _registerP<T>(o: CancelablePromise<T>): CancelablePromise<T> {
this._register(toDisposable(() => o.cancel()));
return o;

View File

@@ -1895,17 +1895,6 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
run: () => this.commandService.executeCommand('workbench.extensions.action.addToWorkspaceIgnoredRecommendations')
});
this.registerExtensionAction({
id: 'workbench.extensions.action.resetStronglyRecommendedIgnoreState',
title: localize2('workbench.extensions.action.resetStronglyRecommendedIgnoreState', "Reset Strongly Recommended Extensions Ignore State"),
category: EXTENSIONS_CATEGORY,
menu: {
id: MenuId.CommandPalette,
when: WorkbenchStateContext.notEqualsTo('empty'),
},
run: async (accessor: ServicesAccessor) => accessor.get(IExtensionRecommendationNotificationService).resetStronglyRecommendedIgnoreState()
});
this.registerExtensionAction({
id: ConfigureWorkspaceRecommendedExtensionsAction.ID,
title: { value: ConfigureWorkspaceRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace)' },

View File

@@ -1,99 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { $, addDisposableListener } from '../../../../base/browser/dom.js';
import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js';
import { Emitter } from '../../../../base/common/event.js';
import { DisposableStore } from '../../../../base/common/lifecycle.js';
import { localize } from '../../../../nls.js';
import { ICustomDialogButtonControl } from '../../../../platform/dialogs/common/dialogs.js';
import { defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js';
export interface StronglyRecommendedExtensionEntry {
readonly displayName: string;
readonly publisherDisplayName: string;
readonly version: string;
}
export interface StronglyRecommendedExtensionListResult<T> {
readonly checkboxStates: ReadonlyMap<T, boolean>;
readonly hasSelection: boolean;
readonly doNotShowAgainUnlessMajorVersionChange: () => boolean;
styleInstallButton(button: ICustomDialogButtonControl): void;
}
export function renderStronglyRecommendedExtensionList<T extends StronglyRecommendedExtensionEntry>(
container: HTMLElement,
disposables: DisposableStore,
extensions: readonly T[],
): StronglyRecommendedExtensionListResult<T> {
const checkboxStates = new Map<T, boolean>();
const onSelectionChanged = disposables.add(new Emitter<void>());
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '8px';
container.style.padding = '8px 0';
const updateCheckbox = (ext: T, cb: Checkbox) => {
checkboxStates.set(ext, cb.checked);
onSelectionChanged.fire();
};
for (const ext of extensions) {
checkboxStates.set(ext, true);
const row = container.appendChild($('.strongly-recommended-extension-row'));
row.style.display = 'flex';
row.style.alignItems = 'center';
row.style.gap = '8px';
const cb = disposables.add(new Checkbox(ext.displayName, true, defaultCheckboxStyles));
disposables.add(cb.onChange(() => updateCheckbox(ext, cb)));
row.appendChild(cb.domNode);
const label = row.appendChild($('span'));
label.textContent = `${ext.displayName} v${ext.version} \u2014 ${ext.publisherDisplayName}`;
label.style.cursor = 'pointer';
disposables.add(addDisposableListener(label, 'click', () => {
cb.checked = !cb.checked;
updateCheckbox(ext, cb);
}));
}
const separator = container.appendChild($('div'));
separator.style.borderTop = '1px solid var(--vscode-widget-border)';
separator.style.marginTop = '4px';
separator.style.paddingTop = '4px';
const doNotShowRow = container.appendChild($('.strongly-recommended-do-not-show-row'));
doNotShowRow.style.display = 'flex';
doNotShowRow.style.alignItems = 'center';
doNotShowRow.style.gap = '8px';
const doNotShowCb = disposables.add(new Checkbox(
localize('doNotShowAgainUnlessMajorVersionChange', "Do not show again unless major version change"),
false,
defaultCheckboxStyles,
));
doNotShowRow.appendChild(doNotShowCb.domNode);
const doNotShowLabel = doNotShowRow.appendChild($('span'));
doNotShowLabel.textContent = localize('doNotShowAgainUnlessMajorVersionChange', "Do not show again unless major version change");
doNotShowLabel.style.cursor = 'pointer';
disposables.add(addDisposableListener(doNotShowLabel, 'click', () => { doNotShowCb.checked = !doNotShowCb.checked; }));
const hasSelection = () => [...checkboxStates.values()].some(v => v);
return {
checkboxStates,
get hasSelection() { return hasSelection(); },
doNotShowAgainUnlessMajorVersionChange: () => doNotShowCb.checked,
styleInstallButton(button: ICustomDialogButtonControl) {
const updateEnabled = () => { button.enabled = hasSelection(); };
disposables.add(onSelectionChanged.event(updateEnabled));
},
};
}

View File

@@ -25,9 +25,6 @@ export class WorkspaceRecommendations extends ExtensionRecommendations {
private _recommendations: ExtensionRecommendation[] = [];
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
private _stronglyRecommended: Array<string | URI> = [];
get stronglyRecommended(): ReadonlyArray<string | URI> { return this._stronglyRecommended; }
private _onDidChangeRecommendations = this._register(new Emitter<void>());
readonly onDidChangeRecommendations = this._onDidChangeRecommendations.event;
@@ -35,7 +32,6 @@ export class WorkspaceRecommendations extends ExtensionRecommendations {
get ignoredRecommendations(): ReadonlyArray<string> { return this._ignoredRecommendations; }
private workspaceExtensions: URI[] = [];
private workspaceExtensionIds = new Map<string, URI>();
private readonly onDidChangeWorkspaceExtensionsScheduler: RunOnceScheduler;
constructor(
@@ -94,12 +90,8 @@ export class WorkspaceRecommendations extends ExtensionRecommendations {
// ignore
}
}
this.workspaceExtensionIds.clear();
if (workspaceExtensions.length) {
const resourceExtensions = await this.workbenchExtensionManagementService.getExtensions(workspaceExtensions);
for (const ext of resourceExtensions) {
this.workspaceExtensionIds.set(ext.identifier.id.toLowerCase(), ext.location);
}
return resourceExtensions.map(extension => extension.location);
}
return [];
@@ -118,7 +110,6 @@ export class WorkspaceRecommendations extends ExtensionRecommendations {
}
this._recommendations = [];
this._stronglyRecommended = [];
this._ignoredRecommendations = [];
for (const extensionsConfig of extensionsConfigs) {
@@ -142,24 +133,6 @@ export class WorkspaceRecommendations extends ExtensionRecommendations {
}
}
}
if (extensionsConfig.stronglyRecommended) {
for (const extensionId of extensionsConfig.stronglyRecommended) {
if (invalidRecommendations.indexOf(extensionId) === -1) {
const workspaceExtUri = this.workspaceExtensionIds.get(extensionId.toLowerCase());
const extension = workspaceExtUri ?? extensionId;
const reason = {
reasonId: ExtensionRecommendationReason.Workspace,
reasonText: localize('stronglyRecommendedExtension', "This extension is strongly recommended by users of the current workspace.")
};
this._stronglyRecommended.push(extension);
if (workspaceExtUri) {
this._recommendations.push({ extension: workspaceExtUri, reason });
} else {
this._recommendations.push({ extension: extensionId, reason });
}
}
}
}
}
for (const extension of this.workspaceExtensions) {
@@ -179,7 +152,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations {
const invalidExtensions: string[] = [];
let message = '';
const allRecommendations = distinct(contents.flatMap(({ recommendations, stronglyRecommended }) => [...(recommendations || []), ...(stronglyRecommended || [])]));
const allRecommendations = distinct(contents.flatMap(({ recommendations }) => recommendations || []));
const regEx = new RegExp(EXTENSION_IDENTIFIER_PATTERN);
for (const extensionId of allRecommendations) {
if (regEx.test(extensionId)) {

View File

@@ -25,15 +25,6 @@ export const ExtensionsConfigurationSchema: IJSONSchema = {
errorMessage: localize('app.extension.identifier.errorMessage', "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.")
},
},
stronglyRecommended: {
type: 'array',
description: localize('app.extensions.json.stronglyRecommended', "List of extensions that are strongly recommended for users of this workspace. Users will be prompted with a dialog to install these extensions. The identifier of an extension is always '${publisher}.${name}'. For example: 'vscode.csharp'."),
items: {
type: 'string',
pattern: EXTENSION_IDENTIFIER_PATTERN,
errorMessage: localize('app.extension.identifier.errorMessage', "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.")
},
},
unwantedRecommendations: {
type: 'array',
description: localize('app.extensions.json.unwantedRecommendations', "List of extensions recommended by VS Code that should not be recommended for users of this workspace. The identifier of an extension is always '${publisher}.${name}'. For example: 'vscode.csharp'."),

View File

@@ -24,7 +24,6 @@ export const EXTENSIONS_CONFIG = '.vscode/extensions.json';
export interface IExtensionsConfigContent {
recommendations?: string[];
stronglyRecommended?: string[];
unwantedRecommendations?: string[];
}
@@ -36,7 +35,6 @@ export interface IWorkspaceExtensionsConfigService {
readonly onDidChangeExtensionsConfigs: Event<void>;
getExtensionsConfigs(): Promise<IExtensionsConfigContent[]>;
getRecommendations(): Promise<string[]>;
getStronglyRecommended(): Promise<string[]>;
getUnwantedRecommendations(): Promise<string[]>;
toggleRecommendation(extensionId: string): Promise<void>;
@@ -86,11 +84,6 @@ export class WorkspaceExtensionsConfigService extends Disposable implements IWor
return distinct(configs.flatMap(c => c.recommendations ? c.recommendations.map(c => c.toLowerCase()) : []));
}
async getStronglyRecommended(): Promise<string[]> {
const configs = await this.getExtensionsConfigs();
return distinct(configs.flatMap(c => c.stronglyRecommended ? c.stronglyRecommended.map(c => c.toLowerCase()) : []));
}
async getUnwantedRecommendations(): Promise<string[]> {
const configs = await this.getExtensionsConfigs();
return distinct(configs.flatMap(c => c.unwantedRecommendations ? c.unwantedRecommendations.map(c => c.toLowerCase()) : []));
@@ -303,7 +296,6 @@ export class WorkspaceExtensionsConfigService extends Disposable implements IWor
private parseExtensionConfig(extensionsConfigContent: IExtensionsConfigContent): IExtensionsConfigContent {
return {
recommendations: distinct((extensionsConfigContent.recommendations || []).map(e => e.toLowerCase())),
stronglyRecommended: distinct((extensionsConfigContent.stronglyRecommended || []).map(e => e.toLowerCase())),
unwantedRecommendations: distinct((extensionsConfigContent.unwantedRecommendations || []).map(e => e.toLowerCase()))
};
}

View File

@@ -1,85 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Dialog } from '../../../../base/browser/ui/dialog/dialog.js';
import { localize } from '../../../../nls.js';
import { defaultButtonStyles, defaultCheckboxStyles, defaultDialogStyles, defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js';
import { StronglyRecommendedExtensionEntry, renderStronglyRecommendedExtensionList } from '../../../contrib/extensions/browser/stronglyRecommendedExtensionList.js';
import { ComponentFixtureContext, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js';
export default defineThemedFixtureGroup({
TwoExtensions: defineComponentFixture({ render: ctx => renderDialog(ctx, twoExtensions) }),
SingleExtension: defineComponentFixture({ render: ctx => renderDialog(ctx, singleExtension) }),
ManyExtensions: defineComponentFixture({ render: ctx => renderDialog(ctx, manyExtensions) }),
NoneSelected: defineComponentFixture({ render: ctx => renderDialog(ctx, twoExtensions, { allUnchecked: true }) }),
});
const twoExtensions: StronglyRecommendedExtensionEntry[] = [
{ displayName: 'TypeScript Customized Language Service', publisherDisplayName: 'Microsoft', version: '3.1.0' },
{ displayName: 'VS Code Extras', publisherDisplayName: 'Microsoft', version: '1.0.5' },
];
const singleExtension: StronglyRecommendedExtensionEntry[] = [
{ displayName: 'TypeScript Customized Language Service', publisherDisplayName: 'Microsoft', version: '3.1.0' },
];
const manyExtensions: StronglyRecommendedExtensionEntry[] = [
{ displayName: 'TypeScript Customized Language Service', publisherDisplayName: 'Microsoft', version: '3.1.0' },
{ displayName: 'VS Code Extras', publisherDisplayName: 'Microsoft', version: '1.0.5' },
{ displayName: 'ESLint', publisherDisplayName: 'Dirk Baeumer', version: '2.4.4' },
{ displayName: 'Prettier', publisherDisplayName: 'Esben Petersen', version: '10.1.0' },
{ displayName: 'GitLens', publisherDisplayName: 'GitKraken', version: '15.6.2' },
];
function renderDialog({ container, disposableStore }: ComponentFixtureContext, extensions: StronglyRecommendedExtensionEntry[], options?: { allUnchecked?: boolean }): void {
container.style.width = '700px';
container.style.height = '500px';
container.style.position = 'relative';
container.style.overflow = 'hidden';
// The dialog uses position:fixed on its modal block, which escapes the shadow DOM container.
// Override to position:absolute so it stays within the fixture bounds.
const fixtureStyle = new CSSStyleSheet();
fixtureStyle.replaceSync('.monaco-dialog-modal-block { position: absolute; }');
const shadowRoot = container.getRootNode() as ShadowRoot;
shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, fixtureStyle];
const message = extensions.length === 1
? localize('strongExtensionFixture', "This workspace strongly recommends installing the '{0}' extension. Do you want to install?", extensions[0].displayName)
: localize('strongExtensionsFixture', "This workspace strongly recommends installing {0} extensions. Do you want to install?", extensions.length);
let listResult!: ReturnType<typeof renderStronglyRecommendedExtensionList>;
const dialog = disposableStore.add(new Dialog(
container,
message,
[
localize('install', "Install"),
localize('cancel', "Cancel"),
],
{
type: 'info',
renderBody: (bodyContainer: HTMLElement) => {
listResult = renderStronglyRecommendedExtensionList(bodyContainer, disposableStore, extensions);
},
buttonOptions: [{
styleButton: (button) => listResult.styleInstallButton(button),
}],
cancelId: 1,
buttonStyles: defaultButtonStyles,
checkboxStyles: defaultCheckboxStyles,
inputBoxStyles: defaultInputBoxStyles,
dialogStyles: defaultDialogStyles,
}
));
dialog.show();
if (options?.allUnchecked) {
for (const cb of container.querySelectorAll<HTMLElement>('.strongly-recommended-extension-row .monaco-custom-toggle')) {
cb.click();
}
}
}