diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index b42d6a194a7e6..26034cdf7dec3 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -162,8 +162,14 @@ } .settings-editor > .settings-body .settings-tree-container .setting-item-extension-toggle .setting-item-extension-toggle-button { - display: block; + display: inline-block; + width: fit-content; +} + +.settings-editor > .settings-body .settings-tree-container .setting-item-extension-toggle .setting-item-extension-dismiss-button { + display: inline-block; width: fit-content; + margin-left: 8px; } .settings-editor.no-results > .settings-body .settings-toc-container, diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 3584e152a210a..73abe4401ca44 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -218,6 +218,9 @@ export class SettingsEditor2 extends EditorPane { private dimension!: DOM.Dimension; private installedExtensionIds: string[] = []; + private dismissedExtensionSettings: string[] = []; + + private readonly DISMISSED_EXTENSION_SETTINGS_STORAGE_KEY = 'settingsEditor2.dismissedExtensionSettings'; private readonly inputChangeListener: MutableDisposable; @@ -265,6 +268,8 @@ export class SettingsEditor2 extends EditorPane { this.editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, SETTINGS_EDITOR_STATE_KEY); + this.dismissedExtensionSettings = this.storageService.get(this.DISMISSED_EXTENSION_SETTINGS_STORAGE_KEY, StorageScope.PROFILE, '').split('\t'); + this._register(configurationService.onDidChangeConfiguration(e => { if (e.source !== ConfigurationTarget.DEFAULT) { this.onConfigUpdate(e.affectedKeys); @@ -286,6 +291,13 @@ export class SettingsEditor2 extends EditorPane { } })); + this._register(extensionManagementService.onDidInstallExtensions(() => { + this.refreshInstalledExtensionsList(); + })); + this._register(extensionManagementService.onDidUninstallExtension(() => { + this.refreshInstalledExtensionsList(); + })); + this.modelDisposables = this._register(new DisposableStore()); if (ENABLE_LANGUAGE_FILTER && !SettingsEditor2.SUGGESTIONS.includes(`@${LANGUAGE_SETTING_TAG}`)) { @@ -399,7 +411,7 @@ export class SettingsEditor2 extends EditorPane { private async refreshInstalledExtensionsList(): Promise { const installedExtensions = await this.extensionManagementService.getInstalled(); this.installedExtensionIds = installedExtensions - .filter(ext => ext.manifest && ext.manifest.contributes && ext.manifest.contributes.configuration) + .filter(ext => ext.manifest.contributes?.configuration) .map(ext => ext.identifier.id); } @@ -679,6 +691,16 @@ export class SettingsEditor2 extends EditorPane { this.onConfigUpdate(undefined, true); } + private onDidDismissExtensionSetting(extensionId: string): void { + if (this.dismissedExtensionSettings.includes(extensionId)) { + return; + } + + this.dismissedExtensionSettings.push(extensionId); + this.storageService.store(this.DISMISSED_EXTENSION_SETTINGS_STORAGE_KEY, this.dismissedExtensionSettings.join('\t'), StorageScope.PROFILE, StorageTarget.USER); + this.onConfigUpdate(undefined, true); + } + private onDidClickSetting(evt: ISettingLinkClickEvent, recursed?: boolean): void { const targetElement = this.currentSettingsModel.getElementsByName(evt.targetKey)?.[0]; let revealFailed = false; @@ -916,6 +938,7 @@ export class SettingsEditor2 extends EditorPane { private createSettingsTree(container: HTMLElement): void { this.settingRenderers = this._register(this.instantiationService.createInstance(SettingTreeRenderers)); this._register(this.settingRenderers.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value, e.type, e.manualReset, e.scope))); + this._register(this.settingRenderers.onDidDismissExtensionSetting((e) => this.onDidDismissExtensionSetting(e))); this._register(this.settingRenderers.onDidOpenSettings(settingKey => { this.openSettingsFile({ revealSetting: { key: settingKey, edit: true } }); })); @@ -1210,43 +1233,6 @@ export class SettingsEditor2 extends EditorPane { }); } - private addOrRemoveManageExtensionSetting(setting: ISetting, extension: IGalleryExtension, groups: ISettingsGroup[]): ISettingsGroup | undefined { - const matchingGroups = groups.filter(g => { - const lowerCaseId = g.extensionInfo?.id.toLowerCase(); - return (lowerCaseId === setting.stableExtensionId!.toLowerCase() || - lowerCaseId === setting.prereleaseExtensionId!.toLowerCase()); - }); - - const extensionId = setting.displayExtensionId!; - const extensionInstalled = this.installedExtensionIds.includes(extensionId); - if (!matchingGroups.length && !extensionInstalled) { - // Only show the recommendation when the extension hasn't been installed. - const newGroup: ISettingsGroup = { - sections: [{ - settings: [setting], - }], - id: extensionId, - title: setting.extensionGroupTitle!, - titleRange: nullRange, - range: nullRange, - extensionInfo: { - id: extensionId, - displayName: extension?.displayName, - } - }; - groups.push(newGroup); - return newGroup; - } else if (matchingGroups.length >= 2 || extensionInstalled) { - // Remove the group with the manage extension setting. - const matchingGroupIndex = matchingGroups.findIndex(group => - group.sections.length === 1 && group.sections[0].settings.length === 1 && group.sections[0].settings[0].displayExtensionId); - if (matchingGroupIndex !== -1) { - groups.splice(matchingGroupIndex, 1); - } - } - return undefined; - } - private createSettingsOrderByTocIndex(resolvedSettingsRoot: ITOCEntry): Map { const index = new Map(); function indexSettings(resolvedSettingsRoot: ITOCEntry, counter = 0): number { @@ -1301,11 +1287,37 @@ export class SettingsEditor2 extends EditorPane { } const additionalGroups: ISettingsGroup[] = []; + let setAdditionalGroups = false; const toggleData = await getExperimentalExtensionToggleData(this.extensionGalleryService, this.productService); if (toggleData && groups.filter(g => g.extensionInfo).length) { for (const key in toggleData.settingsEditorRecommendedExtensions) { - const recommendationInfo = toggleData.settingsEditorRecommendedExtensions[key]; - const extension = toggleData.recommendedExtensionsGalleryInfo[key]; + const extension: IGalleryExtension = toggleData.recommendedExtensionsGalleryInfo[key]; + if (!extension) { + continue; + } + + const extensionId = extension.identifier.id; + const extensionInstalled = this.installedExtensionIds.includes(extensionId); + + // Drill down to see whether the group and setting already exist + // and need to be removed. + const matchingGroupIndex = groups.findIndex(g => + g.extensionInfo && g.extensionInfo!.id.toLowerCase() === extensionId.toLowerCase() && + g.sections.length === 1 && g.sections[0].settings.length === 1 && g.sections[0].settings[0].displayExtensionId + ); + if (extensionInstalled || this.dismissedExtensionSettings.includes(extensionId)) { + if (matchingGroupIndex !== -1) { + groups.splice(matchingGroupIndex, 1); + setAdditionalGroups = true; + } + continue; + } + + if (matchingGroupIndex !== -1) { + continue; + } + + // Create the entry. extensionInstalled is false in this case. let manifest: IExtensionManifest | null = null; try { manifest = await this.extensionGalleryService.getManifest(extension, CancellationToken.None); @@ -1323,7 +1335,8 @@ export class SettingsEditor2 extends EditorPane { groupTitle = contributesConfiguration[0].title; } - const extensionName = extension?.displayName ?? extension?.name ?? extension.identifier.id; + const recommendationInfo = toggleData.settingsEditorRecommendedExtensions[key]; + const extensionName = extension.displayName ?? extension.name ?? extensionId; const settingKey = `${key}.manageExtension`; const setting: ISetting = { range: nullRange, @@ -1336,17 +1349,26 @@ export class SettingsEditor2 extends EditorPane { descriptionRanges: [], scope: ConfigurationScope.WINDOW, type: 'null', - displayExtensionId: extension.identifier.id, - prereleaseExtensionId: key, - stableExtensionId: key, + displayExtensionId: extensionId, extensionGroupTitle: groupTitle ?? extensionName, categoryLabel: 'Extensions', title: extensionName }; - const additionalGroup = this.addOrRemoveManageExtensionSetting(setting, extension, groups); - if (additionalGroup) { - additionalGroups.push(additionalGroup); - } + const additionalGroup: ISettingsGroup = { + sections: [{ + settings: [setting], + }], + id: extensionId, + title: setting.extensionGroupTitle!, + titleRange: nullRange, + range: nullRange, + extensionInfo: { + id: extensionId, + displayName: extension.displayName, + } + }; + additionalGroups.push(additionalGroup); + setAdditionalGroups = true; } } @@ -1356,7 +1378,7 @@ export class SettingsEditor2 extends EditorPane { const commonlyUsed = resolveSettingsTree(commonlyUsedDataToUse, groups, this.logService); resolvedSettingsRoot.children!.unshift(commonlyUsed.tree); - if (toggleData) { + if (toggleData && setAdditionalGroups) { // Add the additional groups to the model to help with searching. this.defaultSettingsEditorModel.setAdditionalGroups(additionalGroups); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 362e608fe189b..73d7c2e286a5e 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -667,6 +667,7 @@ interface ISettingBoolItemTemplate extends ISettingItemTemplate { interface ISettingExtensionToggleItemTemplate extends ISettingItemTemplate { actionButton: Button; + dismissButton: Button; } interface ISettingTextItemTemplate extends ISettingItemTemplate { @@ -1055,7 +1056,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre } } -export class SettingGroupRenderer implements ITreeRenderer { +class SettingGroupRenderer implements ITreeRenderer { templateId = SETTINGS_ELEMENT_TEMPLATE_ID; renderTemplate(container: HTMLElement): IGroupTitleTemplate { @@ -1622,7 +1623,7 @@ abstract class SettingIncludeExcludeRenderer extends AbstractSettingRenderer imp } } -export class SettingExcludeRenderer extends SettingIncludeExcludeRenderer { +class SettingExcludeRenderer extends SettingIncludeExcludeRenderer { templateId = SETTINGS_EXCLUDE_TEMPLATE_ID; protected override isExclude(): boolean { @@ -1630,7 +1631,7 @@ export class SettingExcludeRenderer extends SettingIncludeExcludeRenderer { } } -export class SettingIncludeRenderer extends SettingIncludeExcludeRenderer { +class SettingIncludeRenderer extends SettingIncludeExcludeRenderer { templateId = SETTINGS_INCLUDE_TEMPLATE_ID; protected override isExclude(): boolean { @@ -1745,7 +1746,7 @@ class SettingMultilineTextRenderer extends AbstractSettingTextRenderer implement } } -export class SettingEnumRenderer extends AbstractSettingRenderer implements ITreeRenderer { +class SettingEnumRenderer extends AbstractSettingRenderer implements ITreeRenderer { templateId = SETTINGS_ENUM_TEMPLATE_ID; renderTemplate(container: HTMLElement): ISettingEnumItemTemplate { @@ -1862,7 +1863,7 @@ const settingsNumberInputBoxStyles = getInputBoxStyle({ inputBorder: settingsNumberInputBorder }); -export class SettingNumberRenderer extends AbstractSettingRenderer implements ITreeRenderer { +class SettingNumberRenderer extends AbstractSettingRenderer implements ITreeRenderer { templateId = SETTINGS_NUMBER_TEMPLATE_ID; renderTemplate(_container: HTMLElement): ISettingNumberItemTemplate { @@ -1916,7 +1917,7 @@ export class SettingNumberRenderer extends AbstractSettingRenderer implements IT } } -export class SettingBoolRenderer extends AbstractSettingRenderer implements ITreeRenderer { +class SettingBoolRenderer extends AbstractSettingRenderer implements ITreeRenderer { templateId = SETTINGS_BOOL_TEMPLATE_ID; renderTemplate(_container: HTMLElement): ISettingBoolItemTemplate { @@ -2011,9 +2012,12 @@ type ManageExtensionClickTelemetryClassification = { comment: 'Event used to gain insights into when users interact with an extension management setting'; }; -export class SettingsExtensionToggleRenderer extends AbstractSettingRenderer implements ITreeRenderer { +class SettingsExtensionToggleRenderer extends AbstractSettingRenderer implements ITreeRenderer { templateId = SETTINGS_EXTENSION_TOGGLE_TEMPLATE_ID; + private readonly _onDidDismissExtensionSetting = this._register(new Emitter()); + readonly onDidDismissExtensionSetting = this._onDidDismissExtensionSetting.event; + renderTemplate(_container: HTMLElement): ISettingExtensionToggleItemTemplate { const common = super.renderCommonTemplate(null, _container, 'extension-toggle'); @@ -2024,9 +2028,18 @@ export class SettingsExtensionToggleRenderer extends AbstractSettingRenderer imp actionButton.element.classList.add('setting-item-extension-toggle-button'); actionButton.label = localize('showExtension', "Show Extension"); + const dismissButton = new Button(common.containerElement, { + title: false, + secondary: true, + ...defaultButtonStyles + }); + dismissButton.element.classList.add('setting-item-extension-dismiss-button'); + dismissButton.label = localize('dismiss', "Dismiss"); + const template: ISettingExtensionToggleItemTemplate = { ...common, - actionButton + actionButton, + dismissButton }; this.addSettingElementFocusHandler(template); @@ -2046,6 +2059,11 @@ export class SettingsExtensionToggleRenderer extends AbstractSettingRenderer imp this._telemetryService.publicLog2<{ extensionId: String }, ManageExtensionClickTelemetryClassification>('ManageExtensionClick', { extensionId }); this._commandService.executeCommand('extension.open', extensionId); })); + + template.elementDisposables.add(template.dismissButton.onDidClick(async () => { + this._telemetryService.publicLog2<{ extensionId: String }, ManageExtensionClickTelemetryClassification>('DismissExtensionClick', { extensionId }); + this._onDidDismissExtensionSetting.fire(extensionId); + })); } } @@ -2055,6 +2073,8 @@ export class SettingTreeRenderers extends Disposable { private readonly _onDidChangeSetting = this._register(new Emitter()); readonly onDidChangeSetting: Event; + readonly onDidDismissExtensionSetting: Event; + readonly onDidOpenSettings: Event; readonly onDidClickSettingLink: Event; @@ -2098,6 +2118,7 @@ export class SettingTreeRenderers extends Disposable { const actionFactory = (setting: ISetting, settingTarget: SettingsTarget) => this.getActionsForSetting(setting, settingTarget); const emptyActionFactory = (_: ISetting) => []; + const extensionRenderer = this._instantiationService.createInstance(SettingsExtensionToggleRenderer, [], emptyActionFactory); const settingRenderers = [ this._instantiationService.createInstance(SettingBoolRenderer, this.settingActions, actionFactory), this._instantiationService.createInstance(SettingNumberRenderer, this.settingActions, actionFactory), @@ -2110,7 +2131,7 @@ export class SettingTreeRenderers extends Disposable { this._instantiationService.createInstance(SettingEnumRenderer, this.settingActions, actionFactory), this._instantiationService.createInstance(SettingObjectRenderer, this.settingActions, actionFactory), this._instantiationService.createInstance(SettingBoolObjectRenderer, this.settingActions, actionFactory), - this._instantiationService.createInstance(SettingsExtensionToggleRenderer, [], emptyActionFactory) + extensionRenderer ]; this.onDidClickOverrideElement = Event.any(...settingRenderers.map(r => r.onDidClickOverrideElement)); @@ -2118,6 +2139,7 @@ export class SettingTreeRenderers extends Disposable { ...settingRenderers.map(r => r.onDidChangeSetting), this._onDidChangeSetting.event ); + this.onDidDismissExtensionSetting = extensionRenderer.onDidDismissExtensionSetting; this.onDidOpenSettings = Event.any(...settingRenderers.map(r => r.onDidOpenSettings)); this.onDidClickSettingLink = Event.any(...settingRenderers.map(r => r.onDidClickSettingLink)); this.onDidFocusSetting = Event.any(...settingRenderers.map(r => r.onDidFocusSetting)); diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index f6a278621e42e..1a5286b621ce5 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -96,8 +96,6 @@ export interface ISetting { // Internal properties allKeysAreBoolean?: boolean; displayExtensionId?: string; - stableExtensionId?: string; - prereleaseExtensionId?: string; title?: string; extensionGroupTitle?: string; internalOrder?: number; diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index 29009d0710eec..d675d3b1f8a70 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -217,7 +217,7 @@ export class Settings2EditorModel extends AbstractSettingsModel implements ISett private readonly _onDidChangeGroups: Emitter = this._register(new Emitter()); readonly onDidChangeGroups: Event = this._onDidChangeGroups.event; - private additionalGroups: ISettingsGroup[] | undefined; + private additionalGroups: ISettingsGroup[] = []; private dirty = false; constructor( @@ -245,11 +245,8 @@ export class Settings2EditorModel extends AbstractSettingsModel implements ISett get settingsGroups(): ISettingsGroup[] { const groups = this._defaultSettings.getSettingsGroups(this.dirty); - if (this.additionalGroups?.length) { - groups.push(...this.additionalGroups); - } this.dirty = false; - return groups; + return [...groups, ...this.additionalGroups]; } /** For programmatically added groups outside of registered configurations */ @@ -506,7 +503,7 @@ export class DefaultSettings extends Disposable { private parse(): ISettingsGroup[] { const settingsGroups = this.getRegisteredGroups(); this.initAllSettingsMap(settingsGroups); - const mostCommonlyUsed = this.getMostCommonlyUsedSettings(settingsGroups); + const mostCommonlyUsed = this.getMostCommonlyUsedSettings(); return [mostCommonlyUsed, ...settingsGroups]; } @@ -539,11 +536,11 @@ export class DefaultSettings extends Disposable { } } - private getMostCommonlyUsedSettings(allSettingsGroups: ISettingsGroup[]): ISettingsGroup { + private getMostCommonlyUsedSettings(): ISettingsGroup { const settings = coalesce(this._mostCommonlyUsedSettingsKeys.map(key => { const setting = this._settingsByName.get(key); if (setting) { - return { + return { description: setting.description, key: setting.key, value: setting.value, @@ -556,12 +553,12 @@ export class DefaultSettings extends Disposable { enum: setting.enum, enumDescriptions: setting.enumDescriptions, descriptionRanges: [] - }; + } satisfies ISetting; } return null; })); - return { + return { id: 'mostCommonlyUsed', range: nullRange, title: nls.localize('commonlyUsed', "Commonly Used"), @@ -571,7 +568,7 @@ export class DefaultSettings extends Disposable { settings } ] - }; + } satisfies ISettingsGroup; } private parseConfig(config: IConfigurationNode, result: ISettingsGroup[], configurations: IConfigurationNode[], settingsGroup?: ISettingsGroup, seenSettings?: { [key: string]: boolean }): ISettingsGroup[] {