mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-02 00:09:30 +01:00
Revert "Adds support for stronglyRecommended extensions. Implements #299039"
This reverts commit 240196b595.
This commit is contained in:
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)' },
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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'."),
|
||||
|
||||
@@ -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()))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user