From 6bbb824f0f00e23ad0a483949a5b567c234dec6e Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Fri, 25 Apr 2025 11:17:43 -0700 Subject: [PATCH] Add Settings editor suggestion div (#247262) --- package.json | 2 +- .../browser/workbench.contribution.ts | 8 ++- .../browser/media/settingsEditor2.css | 17 +++++- .../preferences/browser/settingsEditor2.ts | 60 +++++++++++++++++-- .../preferences/browser/settingsLayout.ts | 22 ------- .../preferences/browser/settingsTreeModels.ts | 22 +------ .../contrib/preferences/common/preferences.ts | 40 +++++++++++++ 7 files changed, 122 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index 2fa288631bb..c4ea33e6073 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.100.0", - "distro": "a789cdbede57897d6f39c398e635d744d7d53e5a", + "distro": "04f9cea428fe222c396078326db8d260481d0e0b", "author": { "name": "Microsoft Corporation" }, diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 60578ec0682..e82e936637e 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -615,10 +615,16 @@ const registry = Registry.as(ConfigurationExtensions.Con localize('settings.editor.ui', "Use the settings UI editor."), localize('settings.editor.json', "Use the JSON file editor."), ], - 'description': localize('settings.editor.desc', "Determines which settings editor to use by default."), + 'description': localize('settings.editor.desc', "Determines which Settings editor to use by default."), 'default': 'ui', 'scope': ConfigurationScope.WINDOW }, + 'workbench.settings.showExperimentalSuggestions': { + 'type': 'boolean', + 'default': false, + 'description': localize('settings.showExperimentalSuggestions', "Controls whether experimental suggestions are shown in the Settings editor. This setting requires a reload to take effect."), + 'tags': ['experimental'] + }, 'workbench.hover.delay': { 'type': 'number', 'description': localize('workbench.hover.delay', "Controls the delay in milliseconds after which the hover is shown for workbench items (ex. some extension provided tree view items). Already visible items may require a refresh before reflecting this setting change."), diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 287134403a9..ca718d43bc6 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -64,12 +64,27 @@ } .settings-editor > .settings-header > .settings-header-controls { - height: 32px; display: flex; + flex-wrap: wrap; border-bottom: solid 1px; margin-top: 10px; } +.settings-editor > .settings-header > .settings-header-controls .settings-suggestions { + flex: 0 0 100%; + width: 100%; + min-height: 20px; +} + +.settings-editor > .settings-header > .settings-header-controls .settings-suggestions a { + color: var(--vscode-badge-foreground); + background: var(--vscode-badge-background); + cursor: pointer; + margin-right: 4px; + padding: 0px 4px 2px; + border-radius: 4px; +} + .settings-editor > .settings-header > .settings-header-controls .settings-target-container { flex: auto; } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 49e22168be0..7acbd9efb85 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -41,7 +41,7 @@ import { ITOCEntry, getCommonlyUsedData, tocData } from './settingsLayout.js'; import { AbstractSettingRenderer, HeightChangeParams, ISettingLinkClickEvent, resolveConfiguredUntrustedSettings, createTocTreeForExtensionSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers } from './settingsTree.js'; import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from './settingsTreeModels.js'; import { createTOCIterator, TOCTree, TOCTreeModel } from './tocTree.js'; -import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, ENABLE_LANGUAGE_FILTER, EXTENSION_FETCH_TIMEOUT_MS, EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, ID_SETTING_TAG, IPreferencesSearchService, ISearchProvider, LANGUAGE_SETTING_TAG, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS, WORKSPACE_TRUST_SETTING_TAG, getExperimentalExtensionToggleData } from '../common/preferences.js'; +import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, ENABLE_LANGUAGE_FILTER, EXTENSION_FETCH_TIMEOUT_MS, EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, ID_SETTING_TAG, IPreferencesSearchService, ISearchProvider, LANGUAGE_SETTING_TAG, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS, WORKSPACE_TRUST_SETTING_TAG, getExperimentalExtensionToggleData, wordifyKey } from '../common/preferences.js'; import { settingsHeaderBorder, settingsSashBorder, settingsTextInputBorder } from '../common/settingsEditorColorRegistry.js'; import { IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { IOpenSettingsOptions, IPreferencesService, ISearchResult, ISetting, ISettingsEditorModel, ISettingsEditorOptions, ISettingsGroup, SettingMatchType, SettingValueType, validateSettingsEditorOptions } from '../../../services/preferences/common/preferences.js'; @@ -70,7 +70,6 @@ import { CodeWindow } from '../../../../base/browser/window.js'; import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js'; import { IAiSettingsSearchService } from '../../../services/aiSettingsSearch/common/aiSettingsSearch.js'; - export const enum SettingsFocusContext { Search, TableOfContents, @@ -169,6 +168,7 @@ export class SettingsEditor2 extends EditorPane { private countElement!: HTMLElement; private controlsElement!: HTMLElement; private settingsTargetsWidget!: SettingsTargetsWidget; + private suggestionsDiv!: HTMLElement; private splitView!: SplitView; @@ -227,6 +227,8 @@ export class SettingsEditor2 extends EditorPane { private readonly inputChangeListener: MutableDisposable; + private readonly searchSuggestionDisposables: DisposableStore = this._register(new DisposableStore()); + constructor( group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @@ -670,6 +672,11 @@ export class SettingsEditor2 extends EditorPane { const headerControlsContainer = DOM.append(this.headerContainer, $('.settings-header-controls')); headerControlsContainer.style.borderColor = asCssVariable(settingsHeaderBorder); + this.suggestionsDiv = DOM.append(headerControlsContainer, $('div.settings-suggestions')); + if (this.configurationService.getValue('workbench.settings.showExperimentalSuggestions') === false) { + this.suggestionsDiv.hidden = true; + } + const targetWidgetContainer = DOM.append(headerControlsContainer, $('.settings-target-container')); this.settingsTargetsWidget = this._register(this.instantiationService.createInstance(SettingsTargetsWidget, targetWidgetContainer, { enableRemoteSettings: true })); this.settingsTargetsWidget.settingsTarget = ConfigurationTarget.USER_LOCAL; @@ -1608,6 +1615,7 @@ export class SettingsEditor2 extends EditorPane { } private async triggerSearch(query: string): Promise { + this.clearSearchSuggestions(); const progressRunner = this.editorProgressService.show(true, 800); this.viewState.tagFilters = new Set(); this.viewState.extensionFilters = new Set(); @@ -1703,7 +1711,7 @@ export class SettingsEditor2 extends EditorPane { } const localResults = await this.localFilterPreferences(query, searchInProgress.token); let remoteResults = null; - if (localResults && !localResults.exactMatch && !searchInProgress.token.isCancellationRequested) { + if ((!localResults || !localResults.exactMatch) && !searchInProgress.token.isCancellationRequested) { remoteResults = await this.remoteSearchPreferences(query, searchInProgress.token); } @@ -1715,7 +1723,7 @@ export class SettingsEditor2 extends EditorPane { // ref https://github.com/microsoft/vscode/issues/224946 this.onDidFinishSearch(); - if (remoteResults) { + if (remoteResults?.filterMatches.length) { if (this.aiSettingsSearchService.isEnabled() && !searchInProgress.token.isCancellationRequested) { const rankedResults = await this.aiSettingsSearchService.getLLMRankedResults(query, searchInProgress.token); if (!searchInProgress.token.isCancellationRequested) { @@ -1723,6 +1731,13 @@ export class SettingsEditor2 extends EditorPane { this.logService.trace('No ranked results found'); } else { this.logService.trace(`Got ranked results ${rankedResults.join(', ')}`); + const combinedResultsKeys = new Set([ + ...(localResults?.filterMatches.map(m => m.setting.key) ?? []), + ...(remoteResults.filterMatches.map(m => m.setting.key)) + ]); + const unlistedResults = rankedResults.filter(r => !combinedResultsKeys.has(r)); + this.logService.trace(`Got unlisted results ${unlistedResults.join(', ')}`); + this.setSearchSuggestions(unlistedResults); } } } @@ -1741,6 +1756,43 @@ export class SettingsEditor2 extends EditorPane { this.renderTree(undefined, true); } + private clearSearchSuggestions(): void { + this.searchSuggestionDisposables.clear(); + this.suggestionsDiv.innerText = ''; + } + + private setSearchSuggestions(suggestions: string[]): void { + this.clearSearchSuggestions(); + + if (suggestions.length === 0) { + return; + } + + this.suggestionsDiv.innerText = localize('suggestionsPrefix', "Did you mean: "); + suggestions.forEach((suggestion, idx) => { + const suggestionLink = document.createElement('a'); + suggestionLink.textContent = wordifyKey(suggestion); + suggestionLink.tabIndex = 0; + suggestionLink.setAttribute('aria-label', suggestion); + this.searchSuggestionDisposables.add(DOM.addDisposableListener(suggestionLink, 'click', (e) => { + e.preventDefault(); + this.searchWidget.setValue(suggestion); + this.focusSearch(); + })); + this.searchSuggestionDisposables.add(DOM.addDisposableListener(suggestionLink, 'keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + this.searchWidget.setValue(suggestion); + this.focusSearch(); + } + })); + this.suggestionsDiv.appendChild(suggestionLink); + if (idx < suggestions.length - 1) { + this.suggestionsDiv.appendChild(document.createTextNode(', ')); + } + }); + } + private localFilterPreferences(query: string, token: CancellationToken): Promise { const localSearchProvider = this.preferencesSearchService.getLocalSearchProvider(query); return this.searchWithProvider(SearchResultIdx.Local, localSearchProvider, token); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index 43df1e38b5f..0733ae0ad50 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -302,25 +302,3 @@ export const tocData: ITOCEntry = { } ] }; - -export const knownAcronyms = new Set(); -[ - 'css', - 'html', - 'scss', - 'less', - 'json', - 'js', - 'ts', - 'ie', - 'id', - 'php', - 'scm', -].forEach(str => knownAcronyms.add(str)); - -export const knownTermMappings = new Map(); -knownTermMappings.set('power shell', 'PowerShell'); -knownTermMappings.set('powershell', 'PowerShell'); -knownTermMappings.set('javascript', 'JavaScript'); -knownTermMappings.set('typescript', 'TypeScript'); -knownTermMappings.set('github', 'GitHub'); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index 629e04c3744..2ae55f17398 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -9,8 +9,8 @@ import { isUndefinedOrNull } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { ConfigurationTarget, IConfigurationValue } from '../../../../platform/configuration/common/configuration.js'; import { SettingsTarget } from './preferencesWidgets.js'; -import { ITOCEntry, knownAcronyms, knownTermMappings, tocData } from './settingsLayout.js'; -import { ENABLE_EXTENSION_TOGGLE_SETTINGS, ENABLE_LANGUAGE_FILTER, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, compareTwoNullableNumbers } from '../common/preferences.js'; +import { ITOCEntry, tocData } from './settingsLayout.js'; +import { ENABLE_EXTENSION_TOGGLE_SETTINGS, ENABLE_LANGUAGE_FILTER, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, compareTwoNullableNumbers, wordifyKey } from '../common/preferences.js'; import { IExtensionSetting, ISearchResult, ISetting, ISettingMatch, SettingMatchType, SettingValueType } from '../../../services/preferences/common/preferences.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { FOLDER_SCOPES, WORKSPACE_SCOPES, REMOTE_MACHINE_SCOPES, LOCAL_MACHINE_SCOPES, IWorkbenchConfigurationService, APPLICATION_SCOPES } from '../../../services/configuration/common/configuration.js'; @@ -727,24 +727,6 @@ export function settingKeyToDisplayFormat(key: string, groupId: string = '', isL return { category, label }; } -function wordifyKey(key: string): string { - key = key - .replace(/\.([a-z0-9])/g, (_, p1) => ` \u203A ${p1.toUpperCase()}`) // Replace dot with spaced '>' - .replace(/([a-z0-9])([A-Z])/g, '$1 $2') // Camel case to spacing, fooBar => foo Bar - .replace(/^[a-z]/g, match => match.toUpperCase()) // Upper casing all first letters, foo => Foo - .replace(/\b\w+\b/g, match => { // Upper casing known acronyms - return knownAcronyms.has(match.toLowerCase()) ? - match.toUpperCase() : - match; - }); - - for (const [k, v] of knownTermMappings) { - key = key.replace(new RegExp(`\\b${k}\\b`, 'gi'), v); - } - - return key; -} - /** * Removes redundant sections of the category label. * A redundant section is a section already reflected in the groupId. diff --git a/src/vs/workbench/contrib/preferences/common/preferences.ts b/src/vs/workbench/contrib/preferences/common/preferences.ts index c2d3fe0ddae..fb9655c44b8 100644 --- a/src/vs/workbench/contrib/preferences/common/preferences.ts +++ b/src/vs/workbench/contrib/preferences/common/preferences.ts @@ -178,3 +178,43 @@ export function compareTwoNullableNumbers(a: number | undefined, b: number | und export const PREVIEW_INDICATOR_DESCRIPTION = localize('previewIndicatorDescription', "Preview setting: this setting controls a new feature that is still under refinement yet ready to use. Feedback is welcome."); export const EXPERIMENTAL_INDICATOR_DESCRIPTION = localize('experimentalIndicatorDescription', "Experimental setting: this setting controls a new feature that is actively being developed and may be unstable. It is subject to change or removal."); + +export const knownAcronyms = new Set(); +[ + 'css', + 'html', + 'scss', + 'less', + 'json', + 'js', + 'ts', + 'ie', + 'id', + 'php', + 'scm', +].forEach(str => knownAcronyms.add(str)); + +export const knownTermMappings = new Map(); +knownTermMappings.set('power shell', 'PowerShell'); +knownTermMappings.set('powershell', 'PowerShell'); +knownTermMappings.set('javascript', 'JavaScript'); +knownTermMappings.set('typescript', 'TypeScript'); +knownTermMappings.set('github', 'GitHub'); + +export function wordifyKey(key: string): string { + key = key + .replace(/\.([a-z0-9])/g, (_, p1) => ` \u203A ${p1.toUpperCase()}`) // Replace dot with spaced '>' + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') // Camel case to spacing, fooBar => foo Bar + .replace(/^[a-z]/g, match => match.toUpperCase()) // Upper casing all first letters, foo => Foo + .replace(/\b\w+\b/g, match => { // Upper casing known acronyms + return knownAcronyms.has(match.toLowerCase()) ? + match.toUpperCase() : + match; + }); + + for (const [k, v] of knownTermMappings) { + key = key.replace(new RegExp(`\\b${k}\\b`, 'gi'), v); + } + + return key; +}