From 1aaa1006aa7dc9dc43d1e4ecaf5d68a178ad911e Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 11 Jan 2018 17:44:54 -0800 Subject: [PATCH] New settings search UX - #40957 (#40958) * Basic show fuzzy results below filtered results * Renders filtered settings without a group * Display both local filter and remote results in their own groups on the same page * Remove duplicates between local and remote results * Hide empty groups more completely, port score bounds change from master, fix NPE * Remove fuzzy settings search toggle lightbulb icon * Render all filtered groups as dynamically sized groups * Remove renderer knowledge of filtered/nlp * Update setting count on each search * Fix match ranges for default settings vs the editable settings model * Add ISearchProvider to fix layer breakage. Fix hidden areas for filtered groups * Fix result count and updating on editable side changes * Simplify model rendering - render all result groups dynamically, without allocating extra space * Fix @-searches, clean up filterSettings() code in Settings Model * Update renderers for editable prefs model * Fix up metadata and telemetry * Fix clearing the results by deleting the query * Fix duplicated commonlyUsed settings, and navigation order * Fix nlp results order, and allow scoring results * Remove unused memento * Match count tweaks * Remove obsolete padTo argument to pushGroups * Move searchResultGroup state from renderer to preferencesModels * Remove old fuzzy search prompt link * Fix NPE on filterResult * nlp/filter => remote/local * Remove "render" term from preferencesModels * Simplify settings editor model rendering - All groups are wrapped in braces for consistency When search isn't active, the search groups are removed from the editor model, not hidden by the renderer * Remove unread 'arrays' * Simplify hiding duplicate settings search results * Fix blinking on slow tokenization in search reuslts --- .../preferences/browser/preferencesEditor.ts | 225 ++++++------ .../browser/preferencesRenderers.ts | 187 +++++----- .../preferences/browser/preferencesWidgets.ts | 55 +-- .../parts/preferences/common/preferences.ts | 35 +- .../preferences/common/preferencesModels.ts | 329 +++++++++++------- .../electron-browser/preferencesSearch.ts | 144 ++++---- 6 files changed, 479 insertions(+), 496 deletions(-) diff --git a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index 5525dd14aad..3f5312baea0 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts @@ -6,7 +6,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; import URI from 'vs/base/common/uri'; -import { onUnexpectedError } from 'vs/base/common/errors'; +import { onUnexpectedError, isPromiseCanceledError, getErrorMessage } from 'vs/base/common/errors'; import * as DOM from 'vs/base/browser/dom'; import { Delayer, ThrottledDelayer } from 'vs/base/common/async'; import { Dimension, Builder } from 'vs/base/browser/builder'; @@ -14,7 +14,6 @@ import { ArrayNavigator, INavigator } from 'vs/base/common/iterator'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { SideBySideEditorInput, EditorOptions, EditorInput } from 'vs/workbench/common/editor'; -import { Scope } from 'vs/workbench/common/memento'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; import { IEditorControl, Position, Verbosity } from 'vs/platform/editor/common/editor'; @@ -25,7 +24,7 @@ import { CodeEditor } from 'vs/editor/browser/codeEditor'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IPreferencesService, ISettingsGroup, ISetting, IFilterResult, IPreferencesSearchService, - CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_SEARCH_FOCUS, SETTINGS_EDITOR_COMMAND_SEARCH, SETTINGS_EDITOR_COMMAND_FOCUS_FILE, ISettingsEditorModel, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_FOCUS_NEXT_SETTING, SETTINGS_EDITOR_COMMAND_FOCUS_PREVIOUS_SETTING, IFilterMetadata, IPreferencesSearchModel + CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_SEARCH_FOCUS, SETTINGS_EDITOR_COMMAND_SEARCH, SETTINGS_EDITOR_COMMAND_FOCUS_FILE, ISettingsEditorModel, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_FOCUS_NEXT_SETTING, SETTINGS_EDITOR_COMMAND_FOCUS_PREVIOUS_SETTING, IFilterMetadata, ISearchProvider, ISearchResult } from 'vs/workbench/parts/preferences/common/preferences'; import { SettingsEditorModel, DefaultSettingsEditorModel } from 'vs/workbench/parts/preferences/common/preferencesModels'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -108,14 +107,13 @@ export class PreferencesEditor extends BaseEditor { private headerContainer: HTMLElement; private searchWidget: SearchWidget; private sideBySidePreferencesWidget: SideBySidePreferencesWidget; - private preferencesRenderers: PreferencesRenderers; + private preferencesRenderers: PreferencesRenderersController; private delayedFilterLogging: Delayer; - private filterThrottle: ThrottledDelayer; + private remoteSearchThrottle: ThrottledDelayer; private latestEmptyFilters: string[] = []; private lastFocusedWidget: SearchWidget | SideBySidePreferencesWidget = null; - private memento: any; constructor( @IPreferencesService private preferencesService: IPreferencesService, @@ -131,8 +129,7 @@ export class PreferencesEditor extends BaseEditor { this.defaultSettingsEditorContextKey = CONTEXT_SETTINGS_EDITOR.bindTo(this.contextKeyService); this.focusSettingsContextKey = CONTEXT_SETTINGS_SEARCH_FOCUS.bindTo(this.contextKeyService); this.delayedFilterLogging = new Delayer(1000); - this.filterThrottle = new ThrottledDelayer(200); - this.memento = this.getMemento(storageService, Scope.WORKSPACE); + this.remoteSearchThrottle = new ThrottledDelayer(200); } public createEditor(parent: Builder): void { @@ -145,12 +142,8 @@ export class PreferencesEditor extends BaseEditor { ariaLabel: nls.localize('SearchSettingsWidget.AriaLabel', "Search settings"), placeholder: nls.localize('SearchSettingsWidget.Placeholder', "Search Settings"), focusKey: this.focusSettingsContextKey, - showFuzzyToggle: true, showResultCount: true })); - this.searchWidget.setFuzzyToggleVisible(this.preferencesSearchService.remoteSearchAllowed); - this.searchWidget.fuzzyEnabled = this.memento['fuzzyEnabled']; - this._register(this.preferencesSearchService.onRemoteSearchEnablementChanged(enabled => this.searchWidget.setFuzzyToggleVisible(enabled))); this._register(this.searchWidget.onDidChange(value => this.onInputChanged())); this._register(this.searchWidget.onFocus(() => this.lastFocusedWidget = this.searchWidget)); this.lastFocusedWidget = this.searchWidget; @@ -160,12 +153,7 @@ export class PreferencesEditor extends BaseEditor { this._register(this.sideBySidePreferencesWidget.onFocus(() => this.lastFocusedWidget = this.sideBySidePreferencesWidget)); this._register(this.sideBySidePreferencesWidget.onDidSettingsTargetChange(target => this.switchSettings(target))); - this.preferencesRenderers = this._register(new PreferencesRenderers(this.preferencesSearchService)); - - this._register(this.preferencesRenderers.onTriggeredFuzzy(() => { - this.searchWidget.fuzzyEnabled = true; - this.filterPreferences(); - })); + this.preferencesRenderers = this._register(new PreferencesRenderersController(this.preferencesSearchService, this.telemetryService)); this._register(this.preferencesRenderers.onDidFilterResultsCountChange(count => this.showSearchResultsMessage(count))); } @@ -250,15 +238,17 @@ export class PreferencesEditor extends BaseEditor { } private onInputChanged(): void { - if (this.searchWidget.fuzzyEnabled) { - this.triggerThrottledFilter(); - } else { - this.filterPreferences(); - } + this.triggerThrottledSearch(); + this.localFilterPreferences(); } - private triggerThrottledFilter(): void { - this.filterThrottle.trigger(() => this.filterPreferences()); + private triggerThrottledSearch(): void { + if (this.searchWidget.getValue()) { + this.remoteSearchThrottle.trigger(() => this.remoteSearchPreferences()); + } else { + // When clearing the input, update immediately to clear it + this.remoteSearchPreferences(); + } } private switchSettings(target: SettingsTarget): void { @@ -278,17 +268,29 @@ export class PreferencesEditor extends BaseEditor { }); } - private filterPreferences(): TPromise { - this.memento['fuzzyEnabled'] = this.searchWidget.fuzzyEnabled; - const filter = this.searchWidget.getValue().trim(); - return this.preferencesRenderers.filterPreferences({ filter, fuzzy: this.searchWidget.fuzzyEnabled }).then(result => { + private remoteSearchPreferences(): TPromise { + const query = this.searchWidget.getValue().trim(); + return this.preferencesRenderers.remoteSearchPreferences(query).then(result => { + this.onSearchResult(query, result); + }, onUnexpectedError); + } + + private localFilterPreferences(): TPromise { + const query = this.searchWidget.getValue().trim(); + return this.preferencesRenderers.localFilterPreferences(query).then(result => { + this.onSearchResult(query, result); + }, onUnexpectedError); + } + + private onSearchResult(filter: string, result: { count: number, metadata: IFilterMetadata }): void { + if (result) { this.showSearchResultsMessage(result.count); if (result.count === 0) { this.latestEmptyFilters.push(filter); } - this.preferencesRenderers.focusFirst(); + this.delayedFilterLogging.trigger(() => this.reportFilteringUsed(filter, result.metadata)); - }, onUnexpectedError); + } } private showSearchResultsMessage(count: number): void { @@ -372,35 +374,32 @@ class SettingsNavigator implements INavigator { } } -interface ISearchCriteria { - filter: string; - fuzzy: boolean; +interface IFilterOrSearchResult { + count: number; + metadata: IFilterMetadata; } -class PreferencesRenderers extends Disposable { +class PreferencesRenderersController extends Disposable { private _defaultPreferencesRenderer: IPreferencesRenderer; private _defaultPreferencesRendererDisposables: IDisposable[] = []; - private _defaultPreferencesFilterResult: IFilterResult; - private _editablePreferencesFilterResult: IFilterResult; - private _editablePreferencesRenderer: IPreferencesRenderer; private _editablePreferencesRendererDisposables: IDisposable[] = []; private _settingsNavigator: SettingsNavigator; private _filtersInProgress: TPromise[]; - private _searchCriteria: ISearchCriteria; - private _currentSearchModel: IPreferencesSearchModel; - private _onTriggeredFuzzy: Emitter = this._register(new Emitter()); - public onTriggeredFuzzy: Event = this._onTriggeredFuzzy.event; + private _currentLocalSearchProvider: ISearchProvider; + private _currentRemoteSearchProvider: ISearchProvider; + private _lastQuery: string; private _onDidFilterResultsCountChange: Emitter = this._register(new Emitter()); public onDidFilterResultsCountChange: Event = this._onDidFilterResultsCountChange.event; constructor( - private preferencesSearchService: IPreferencesSearchService + private preferencesSearchService: IPreferencesSearchService, + private telemetryService: ITelemetryService ) { super(); } @@ -423,9 +422,6 @@ class PreferencesRenderers extends Disposable { this._defaultPreferencesRenderer.onUpdatePreference(({ key, value, source, index }) => this._updatePreference(key, value, source, index, this._editablePreferencesRenderer), this, this._defaultPreferencesRendererDisposables); this._defaultPreferencesRenderer.onFocusPreference(preference => this._focusPreference(preference, this._editablePreferencesRenderer), this, this._defaultPreferencesRendererDisposables); this._defaultPreferencesRenderer.onClearFocusPreference(preference => this._clearFocus(preference, this._editablePreferencesRenderer), this, this._defaultPreferencesRendererDisposables); - if (this._defaultPreferencesRenderer.onTriggeredFuzzy) { - this._register(this._defaultPreferencesRenderer.onTriggeredFuzzy(() => this._onTriggeredFuzzy.fire())); - } } } } @@ -435,42 +431,45 @@ class PreferencesRenderers extends Disposable { this._editablePreferencesRenderer = editableSettingsRenderer; this._editablePreferencesRendererDisposables = dispose(this._editablePreferencesRendererDisposables); if (this._editablePreferencesRenderer) { - (this._editablePreferencesRenderer.preferencesModel).onDidChangeGroups(() => { - if (this._currentSearchModel) { - this._filterEditablePreferences() - .then(() => { - const count = this.consolidateAndUpdate(); - this._onDidFilterResultsCountChange.fire(count); - }); - } - }, this, this._editablePreferencesRendererDisposables); + (this._editablePreferencesRenderer.preferencesModel) + .onDidChangeGroups(this._onEditableContentDidChange, this, this._editablePreferencesRendererDisposables); } } } - filterPreferences(criteria: ISearchCriteria): TPromise<{ count: number, metadata: IFilterMetadata }> { - this._searchCriteria = criteria; + async _onEditableContentDidChange(): TPromise { + await this.localFilterPreferences(this._lastQuery, true); + await this.remoteSearchPreferences(this._lastQuery, true); + } + remoteSearchPreferences(query: string, updateCurrentResults?: boolean): TPromise { + this._currentRemoteSearchProvider = (updateCurrentResults && this._currentRemoteSearchProvider) || this.preferencesSearchService.getRemoteSearchProvider(query); + return this.filterOrSearchPreferences(query, this._currentRemoteSearchProvider, 'nlpResult', nls.localize('nlpResult', "Natural Language Results")); + } + + localFilterPreferences(query: string, updateCurrentResults?: boolean): TPromise { + this._currentLocalSearchProvider = (updateCurrentResults && this._currentLocalSearchProvider) || this.preferencesSearchService.getLocalSearchProvider(query); + return this.filterOrSearchPreferences(query, this._currentLocalSearchProvider, 'filterResult', nls.localize('filterResult', "Filtered Results")); + } + + filterOrSearchPreferences(query: string, searchProvider: ISearchProvider, groupId: string, groupLabel: string): TPromise { + this._lastQuery = query; if (this._filtersInProgress) { // Resolved/rejected promises have no .cancel() this._filtersInProgress.forEach(p => p.cancel && p.cancel()); } - this._currentSearchModel = this.preferencesSearchService.startSearch(this._searchCriteria.filter, criteria.fuzzy); - this._filtersInProgress = [this._filterDefaultPreferences(), this._filterEditablePreferences()]; - - return TPromise.join(this._filtersInProgress).then(() => { - const count = this.consolidateAndUpdate(); - return { count, metadata: this._defaultPreferencesFilterResult && this._defaultPreferencesFilterResult.metadata }; - }); - } + this._filtersInProgress = [ + this._filterOrSearchPreferences(query, this.defaultPreferencesRenderer, searchProvider, groupId, groupLabel), + this._filterOrSearchPreferences(query, this.editablePreferencesRenderer, searchProvider, groupId, groupLabel)]; - focusFirst(): void { - // Focus first match in both renderers - this._focusPreference(this._getFirstSettingFromTheGroups(this._defaultPreferencesFilterResult ? this._defaultPreferencesFilterResult.filteredGroups : []), this._defaultPreferencesRenderer); - this._focusPreference(this._getFirstSettingFromTheGroups(this._editablePreferencesFilterResult ? this._editablePreferencesFilterResult.filteredGroups : []), this._editablePreferencesRenderer); + return TPromise.join(this._filtersInProgress).then(results => { + this._filtersInProgress = null; + const [defaultFilterResult, editableFilterResult] = results; - this._settingsNavigator.first(); // Move to first + const count = this.consolidateAndUpdate(defaultFilterResult, editableFilterResult); + return { count, metadata: defaultFilterResult && defaultFilterResult.metadata }; + }); } focusNextPreference(forward: boolean = true) { @@ -483,56 +482,61 @@ class PreferencesRenderers extends Disposable { this._focusPreference(setting, this._editablePreferencesRenderer); } - private _filterDefaultPreferences(): TPromise { - if (this._searchCriteria && this._defaultPreferencesRenderer) { - return this._filterPreferences(this._searchCriteria, this._defaultPreferencesRenderer, this._currentSearchModel) - .then(filterResult => { this._defaultPreferencesFilterResult = filterResult; }); - } - return TPromise.wrap(null); - } + private _filterOrSearchPreferences(filter: string, preferencesRenderer: IPreferencesRenderer, provider: ISearchProvider, groupId: string, groupLabel: string): TPromise { + if (preferencesRenderer) { + const model = preferencesRenderer.preferencesModel; + const searchP = provider ? provider.searchModel(model) : TPromise.wrap(null); + return searchP + .then(null, err => { + if (isPromiseCanceledError(err)) { + return null; + } else { + /* __GDPR__ + "defaultSettings.searchError" : { + "message": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + const message = getErrorMessage(err); + this.telemetryService.publicLog('defaultSettings.searchError', { message }); + return null; + } + }) + .then(searchResult => { + const filterResult = searchResult ? + model.updateResultGroup(groupId, { + id: groupId, + label: groupLabel, + result: searchResult + }) : + model.updateResultGroup(groupId, null); + + if (filterResult) { + filterResult.query = filter; + } - private _filterEditablePreferences(): TPromise { - if (this._searchCriteria && this._editablePreferencesRenderer) { - return this._filterPreferences(this._searchCriteria, this._editablePreferencesRenderer, this._currentSearchModel) - .then(filterResult => { this._editablePreferencesFilterResult = filterResult; }); + preferencesRenderer.filterPreferences(filterResult); + return filterResult; + }); } + return TPromise.wrap(null); } - private _getFirstSettingFromTheGroups(allGroups: ISettingsGroup[]): ISetting { - if (allGroups.length) { - if (allGroups[0].sections.length) { - return allGroups[0].sections[0].settings[0]; - } - } - return null; + private consolidateAndUpdate(defaultFilterResult: IFilterResult, editableFilterResult: IFilterResult): number { + const defaultPreferencesFilteredGroups = defaultFilterResult ? defaultFilterResult.filteredGroups : this._getAllPreferences(this._defaultPreferencesRenderer); + const editablePreferencesFilteredGroups = editableFilterResult ? editableFilterResult.filteredGroups : this._getAllPreferences(this._editablePreferencesRenderer); + const consolidatedSettings = this._consolidateSettings(editablePreferencesFilteredGroups, defaultPreferencesFilteredGroups); + + this._settingsNavigator = new SettingsNavigator(this._lastQuery ? consolidatedSettings : []); + const count = consolidatedSettings.length; + this._onDidFilterResultsCountChange.fire(count); + return count; } private _getAllPreferences(preferencesRenderer: IPreferencesRenderer): ISettingsGroup[] { return preferencesRenderer ? (preferencesRenderer.preferencesModel).settingsGroups : []; } - private _filterPreferences(searchCriteria: ISearchCriteria, preferencesRenderer: IPreferencesRenderer, searchModel: IPreferencesSearchModel): TPromise { - if (preferencesRenderer && searchCriteria) { - const prefSearchP = searchModel.filterPreferences(preferencesRenderer.preferencesModel); - - return prefSearchP.then(filterResult => { - preferencesRenderer.filterPreferences(filterResult, this.preferencesSearchService.remoteSearchAllowed); - return filterResult; - }); - } - return TPromise.as(null); - } - - private consolidateAndUpdate(): number { - const defaultPreferencesFilteredGroups = this._defaultPreferencesFilterResult ? this._defaultPreferencesFilterResult.filteredGroups : this._getAllPreferences(this._defaultPreferencesRenderer); - const editablePreferencesFilteredGroups = this._editablePreferencesFilterResult ? this._editablePreferencesFilterResult.filteredGroups : this._getAllPreferences(this._editablePreferencesRenderer); - const consolidatedSettings = this._consolidateSettings(editablePreferencesFilteredGroups, defaultPreferencesFilteredGroups); - - this._settingsNavigator = new SettingsNavigator(this._searchCriteria.filter ? consolidatedSettings : []); - return consolidatedSettings.length; - } - private _focusPreference(preference: ISetting, preferencesRenderer: IPreferencesRenderer): void { if (preference && preferencesRenderer) { preferencesRenderer.focusPreference(preference); @@ -553,7 +557,7 @@ class PreferencesRenderers extends Disposable { private _consolidateSettings(editableSettingsGroups: ISettingsGroup[], defaultSettingsGroups: ISettingsGroup[]): ISetting[] { const editableSettings = this._flatten(editableSettingsGroups); - const defaultSettings = this._flatten(defaultSettingsGroups).filter(secondarySetting => !editableSettings.some(primarySetting => primarySetting.key === secondarySetting.key)); + const defaultSettings = this._flatten(defaultSettingsGroups).filter(secondarySetting => editableSettings.every(primarySetting => primarySetting.key !== secondarySetting.key)); return [...defaultSettings, ...editableSettings]; } @@ -564,6 +568,7 @@ class PreferencesRenderers extends Disposable { settings.push(...section.settings); } } + return settings; } diff --git a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts index 9cdec8686f6..1ae612e71fd 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts @@ -7,7 +7,6 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; import { Delayer } from 'vs/base/common/async'; import * as strings from 'vs/base/common/strings'; -import { tail } from 'vs/base/common/arrays'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IAction } from 'vs/base/common/actions'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; @@ -42,13 +41,12 @@ export interface IPreferencesRenderer extends IDisposable { onFocusPreference: Event; onClearFocusPreference: Event; onUpdatePreference?: Event<{ key: string, value: any, source: T, index: number }>; - onTriggeredFuzzy?: Event; render(): void; updatePreference(key: string, value: any, source: T, index: number): void; - filterPreferences(filterResult: IFilterResult, fuzzySearchAvailable: boolean): void; focusPreference(setting: T): void; clearFocus(setting: T): void; + filterPreferences(filterResult: IFilterResult): void; } export class UserSettingsRenderer extends Disposable implements IPreferencesRenderer { @@ -162,6 +160,7 @@ export class UserSettingsRenderer extends Disposable implements IPreferencesRend } return null; } + return this.preferencesModel.getPreference(key); } @@ -247,6 +246,8 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR private hiddenAreasRenderer: HiddenAreasRenderer; private editSettingActionRenderer: EditSettingRenderer; private feedbackWidgetRenderer: FeedbackWidgetRenderer; + private bracesHidingRenderer: BracesHidingRenderer; + private filterResult: IFilterResult; private _onUpdatePreference: Emitter<{ key: string, value: any, source: ISetting, index: number }> = new Emitter<{ key: string, value: any, source: ISetting, index: number }>(); public readonly onUpdatePreference: Event<{ key: string, value: any, source: ISetting, index: number }> = this._onUpdatePreference.event; @@ -257,10 +258,6 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR private _onClearFocusPreference: Emitter = new Emitter(); public readonly onClearFocusPreference: Event = this._onClearFocusPreference.event; - public readonly onTriggeredFuzzy: Event; - - private filterResult: IFilterResult; - constructor(protected editor: ICodeEditor, public readonly preferencesModel: DefaultSettingsEditorModel, @IPreferencesService protected preferencesService: IPreferencesService, @IInstantiationService protected instantiationService: IInstantiationService @@ -272,14 +269,12 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR this.filteredMatchesRenderer = this._register(instantiationService.createInstance(FilteredMatchesRenderer, editor)); this.editSettingActionRenderer = this._register(instantiationService.createInstance(EditSettingRenderer, editor, preferencesModel, this.settingHighlighter)); this.feedbackWidgetRenderer = this._register(instantiationService.createInstance(FeedbackWidgetRenderer, editor)); - const parenthesisHidingRenderer = this._register(instantiationService.createInstance(StaticContentHidingRenderer, editor, preferencesModel)); - this.hiddenAreasRenderer = this._register(instantiationService.createInstance(HiddenAreasRenderer, editor, [this.settingsGroupTitleRenderer, this.filteredMatchesRenderer, parenthesisHidingRenderer])); + this.bracesHidingRenderer = this._register(instantiationService.createInstance(BracesHidingRenderer, editor, preferencesModel)); + this.hiddenAreasRenderer = this._register(instantiationService.createInstance(HiddenAreasRenderer, editor, [this.settingsGroupTitleRenderer, this.filteredMatchesRenderer, this.bracesHidingRenderer])); this._register(this.editSettingActionRenderer.onUpdateSetting(e => this._onUpdatePreference.fire(e))); this._register(this.settingsGroupTitleRenderer.onHiddenAreasChanged(() => this.hiddenAreasRenderer.render())); this._register(preferencesModel.onDidChangeGroups(() => this.render())); - - this.onTriggeredFuzzy = this.settingsHeaderRenderer.onClick; } public get associatedPreferencesModel(): IPreferencesEditorModel { @@ -296,18 +291,20 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR this.editSettingActionRenderer.render(this.preferencesModel.settingsGroups, this._associatedPreferencesModel); this.feedbackWidgetRenderer.render(null); this.settingHighlighter.clear(true); + this.bracesHidingRenderer.render(null, this.preferencesModel.settingsGroups); this.settingsGroupTitleRenderer.showGroup(0); this.hiddenAreasRenderer.render(); } - public filterPreferences(filterResult: IFilterResult, fuzzySearchAvailable: boolean): void { + public filterPreferences(filterResult: IFilterResult): void { this.filterResult = filterResult; if (filterResult) { this.filteredMatchesRenderer.render(filterResult, this.preferencesModel.settingsGroups); this.settingsGroupTitleRenderer.render(filterResult.filteredGroups); this.feedbackWidgetRenderer.render(filterResult); - this.settingsHeaderRenderer.render(filterResult, fuzzySearchAvailable); + this.settingsHeaderRenderer.render(filterResult); this.settingHighlighter.clear(true); + this.bracesHidingRenderer.render(filterResult, this.preferencesModel.settingsGroups); this.editSettingActionRenderer.render(filterResult.filteredGroups, this._associatedPreferencesModel); } else { this.settingHighlighter.clear(true); @@ -316,6 +313,7 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR this.settingsHeaderRenderer.render(null); this.settingsGroupTitleRenderer.render(this.preferencesModel.settingsGroups); this.settingsGroupTitleRenderer.showGroup(0); + this.bracesHidingRenderer.render(null, this.preferencesModel.settingsGroups); this.editSettingActionRenderer.render(this.preferencesModel.settingsGroups, this._associatedPreferencesModel); } @@ -372,45 +370,64 @@ export interface HiddenAreasProvider { hiddenAreas: IRange[]; } -export class StaticContentHidingRenderer extends Disposable implements HiddenAreasProvider { +export class BracesHidingRenderer extends Disposable implements HiddenAreasProvider { + private _result: IFilterResult; + private _settingsGroups: ISettingsGroup[]; - constructor(private editor: ICodeEditor, private settingsEditorModel: ISettingsEditorModel - ) { + constructor(private editor: ICodeEditor) { super(); } - get hiddenAreas(): IRange[] { - const model = this.editor.getModel(); + render(result: IFilterResult, settingsGroups: ISettingsGroup[]): void { + this._result = result; + this._settingsGroups = settingsGroups; + } - // Hide extra chars for "search results" and "commonly used" groups - const settingsGroups = this.settingsEditorModel.settingsGroups; - const lastGroup = tail(settingsGroups); - return [ + get hiddenAreas(): IRange[] { + // Opening square brace + const hiddenAreas = [ { startLineNumber: 1, - startColumn: model.getLineMinColumn(1), + startColumn: 1, endLineNumber: 2, - endColumn: model.getLineMaxColumn(2) - }, - { - startLineNumber: settingsGroups[0].range.endLineNumber + 1, - startColumn: model.getLineMinColumn(settingsGroups[0].range.endLineNumber + 1), - endLineNumber: settingsGroups[0].range.endLineNumber + 4, - endColumn: model.getLineMaxColumn(settingsGroups[0].range.endLineNumber + 4) - }, - { - startLineNumber: lastGroup.range.endLineNumber + 1, - startColumn: model.getLineMinColumn(lastGroup.range.endLineNumber + 1), - endLineNumber: Math.min(model.getLineCount(), lastGroup.range.endLineNumber + 4), - endColumn: model.getLineMaxColumn(Math.min(model.getLineCount(), lastGroup.range.endLineNumber + 4)) - }, - { - startLineNumber: model.getLineCount() - 1, - startColumn: model.getLineMinColumn(model.getLineCount() - 1), - endLineNumber: model.getLineCount(), - endColumn: model.getLineMaxColumn(model.getLineCount()) + endColumn: 1 } ]; + + const hideBraces = group => { + // Opening curly brace + hiddenAreas.push({ + startLineNumber: group.range.startLineNumber - 3, + startColumn: 1, + endLineNumber: group.range.startLineNumber - 3, + endColumn: 1 + }); + + // Closing curly brace + hiddenAreas.push({ + startLineNumber: group.range.endLineNumber + 1, + startColumn: 1, + endLineNumber: group.range.endLineNumber + 4, + endColumn: 1 + }); + }; + + this._settingsGroups.forEach(hideBraces); + if (this._result) { + this._result.filteredGroups.forEach(hideBraces); + } + + // Closing square brace + const lineCount = this.editor.getModel().getLineCount(); + hiddenAreas.push({ + startLineNumber: lineCount, + startColumn: 1, + endLineNumber: lineCount, + endColumn: 1 + }); + + + return hiddenAreas; } } @@ -426,10 +443,9 @@ class DefaultSettingsHeaderRenderer extends Disposable { this.onClick = this.settingsHeaderWidget.onClick; } - public render(filterResult: IFilterResult, fuzzySearchAvailable = false) { + public render(filterResult: IFilterResult) { const hasSettings = !filterResult || filterResult.filteredGroups.length > 0; - const promptFuzzy = fuzzySearchAvailable && filterResult && !filterResult.metadata; - this.settingsHeaderWidget.toggleMessage(hasSettings, promptFuzzy); + this.settingsHeaderWidget.toggleMessage(hasSettings); } } @@ -728,7 +744,7 @@ export class FilteredMatchesRenderer extends Disposable implements HiddenAreasPr this.decorationIds = changeAccessor.deltaDecorations(this.decorationIds, result.matches.map(match => this.createDecoration(match, model))); }); } else { - this.hiddenAreas = this.computeHiddenRanges(allSettingsGroups, allSettingsGroups, model); + this.hiddenAreas = this.computeHiddenRanges(null, allSettingsGroups, model); } } @@ -739,65 +755,24 @@ export class FilteredMatchesRenderer extends Disposable implements HiddenAreasPr stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'findMatch' } - }; } private computeHiddenRanges(filteredGroups: ISettingsGroup[], allSettingsGroups: ISettingsGroup[], model: ITextModel): IRange[] { + // Hide the contents of hidden groups const notMatchesRanges: IRange[] = []; - for (const group of allSettingsGroups) { - const filteredGroup = filteredGroups.filter(g => g.title === group.title)[0]; - if (!filteredGroup || filteredGroup.sections.every(sect => sect.settings.length === 0)) { + if (filteredGroups) { + allSettingsGroups.forEach((group, i) => { notMatchesRanges.push({ startLineNumber: group.range.startLineNumber - 1, - startColumn: model.getLineMinColumn(group.range.startLineNumber - 1), + startColumn: group.range.startColumn, endLineNumber: group.range.endLineNumber, - endColumn: model.getLineMaxColumn(group.range.endLineNumber), + endColumn: group.range.endColumn }); - } else { - for (const section of group.sections) { - if (section.titleRange) { - if (!this.containsLine(section.titleRange.startLineNumber, filteredGroup)) { - notMatchesRanges.push(this.createCompleteRange(section.titleRange, model)); - } - } - for (const setting of section.settings) { - if (!this.containsLine(setting.range.startLineNumber, filteredGroup)) { - notMatchesRanges.push(this.createCompleteRange(setting.range, model)); - } - } - } - } - } - return notMatchesRanges; - } - - private containsLine(lineNumber: number, settingsGroup: ISettingsGroup): boolean { - if (settingsGroup.titleRange && lineNumber >= settingsGroup.titleRange.startLineNumber && lineNumber <= settingsGroup.titleRange.endLineNumber) { - return true; - } - - for (const section of settingsGroup.sections) { - if (section.titleRange && lineNumber >= section.titleRange.startLineNumber && lineNumber <= section.titleRange.endLineNumber) { - return true; - } - - for (const setting of section.settings) { - if (lineNumber >= setting.range.startLineNumber && lineNumber <= setting.range.endLineNumber) { - return true; - } - } + }); } - return false; - } - private createCompleteRange(range: IRange, model: ITextModel): IRange { - return { - startLineNumber: range.startLineNumber, - startColumn: model.getLineMinColumn(range.startLineNumber), - endLineNumber: range.endLineNumber, - endColumn: model.getLineMaxColumn(range.endLineNumber) - }; + return notMatchesRanges; } public dispose() { @@ -859,7 +834,7 @@ interface IIndexedSetting extends ISetting { class EditSettingRenderer extends Disposable { - private editPreferenceWidgetForCusorPosition: EditPreferenceWidget; + private editPreferenceWidgetForCursorPosition: EditPreferenceWidget; private editPreferenceWidgetForMouseMove: EditPreferenceWidget; private settingsGroups: ISettingsGroup[]; @@ -876,11 +851,11 @@ class EditSettingRenderer extends Disposable { ) { super(); - this.editPreferenceWidgetForCusorPosition = this._register(this.instantiationService.createInstance(EditPreferenceWidget, editor)); - this.editPreferenceWidgetForMouseMove = this._register(this.instantiationService.createInstance(EditPreferenceWidget, editor)); + this.editPreferenceWidgetForCursorPosition = >this._register(this.instantiationService.createInstance(EditPreferenceWidget, editor)); + this.editPreferenceWidgetForMouseMove = >this._register(this.instantiationService.createInstance(EditPreferenceWidget, editor)); this.toggleEditPreferencesForMouseMoveDelayer = new Delayer(75); - this._register(this.editPreferenceWidgetForCusorPosition.onClick(e => this.onEditSettingClicked(this.editPreferenceWidgetForCusorPosition, e))); + this._register(this.editPreferenceWidgetForCursorPosition.onClick(e => this.onEditSettingClicked(this.editPreferenceWidgetForCursorPosition, e))); this._register(this.editPreferenceWidgetForMouseMove.onClick(e => this.onEditSettingClicked(this.editPreferenceWidgetForMouseMove, e))); this._register(this.editor.onDidChangeCursorPosition(positionChangeEvent => this.onPositionChanged(positionChangeEvent))); @@ -889,14 +864,14 @@ class EditSettingRenderer extends Disposable { } public render(settingsGroups: ISettingsGroup[], associatedPreferencesModel: IPreferencesEditorModel): void { - this.editPreferenceWidgetForCusorPosition.hide(); + this.editPreferenceWidgetForCursorPosition.hide(); this.editPreferenceWidgetForMouseMove.hide(); this.settingsGroups = settingsGroups; this.associatedPreferencesModel = associatedPreferencesModel; const settings = this.getSettings(this.editor.getPosition().lineNumber); if (settings.length) { - this.showEditPreferencesWidget(this.editPreferenceWidgetForCusorPosition, settings); + this.showEditPreferencesWidget(this.editPreferenceWidgetForCursorPosition, settings); } } @@ -906,7 +881,7 @@ class EditSettingRenderer extends Disposable { private onConfigurationChanged(): void { if (!this.editor.getConfiguration().viewInfo.glyphMargin) { - this.editPreferenceWidgetForCusorPosition.hide(); + this.editPreferenceWidgetForCursorPosition.hide(); this.editPreferenceWidgetForMouseMove.hide(); } } @@ -915,9 +890,9 @@ class EditSettingRenderer extends Disposable { this.editPreferenceWidgetForMouseMove.hide(); const settings = this.getSettings(positionChangeEvent.position.lineNumber); if (settings.length) { - this.showEditPreferencesWidget(this.editPreferenceWidgetForCusorPosition, settings); + this.showEditPreferencesWidget(this.editPreferenceWidgetForCursorPosition, settings); } else { - this.editPreferenceWidgetForCusorPosition.hide(); + this.editPreferenceWidgetForCursorPosition.hide(); } } @@ -937,8 +912,8 @@ class EditSettingRenderer extends Disposable { if (this.editPreferenceWidgetForMouseMove.getLine() === line && this.editPreferenceWidgetForMouseMove.isVisible()) { return this.editPreferenceWidgetForMouseMove; } - if (this.editPreferenceWidgetForCusorPosition.getLine() === line && this.editPreferenceWidgetForCusorPosition.isVisible()) { - return this.editPreferenceWidgetForCusorPosition; + if (this.editPreferenceWidgetForCursorPosition.getLine() === line && this.editPreferenceWidgetForCursorPosition.isVisible()) { + return this.editPreferenceWidgetForCursorPosition; } } return null; @@ -957,7 +932,7 @@ class EditSettingRenderer extends Disposable { const line = settings[0].valueRange.startLineNumber; if (this.editor.getConfiguration().viewInfo.glyphMargin && this.marginFreeFromOtherDecorations(line)) { editPreferencesWidget.show(line, nls.localize('editTtile', "Edit"), settings); - const editPreferenceWidgetToHide = editPreferencesWidget === this.editPreferenceWidgetForCusorPosition ? this.editPreferenceWidgetForMouseMove : this.editPreferenceWidgetForCusorPosition; + const editPreferenceWidgetToHide = editPreferencesWidget === this.editPreferenceWidgetForCursorPosition ? this.editPreferenceWidgetForMouseMove : this.editPreferenceWidgetForCursorPosition; editPreferenceWidgetToHide.hide(); } } diff --git a/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts index 7b740eae6ae..78cba03e94d 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts @@ -10,7 +10,6 @@ import * as DOM from 'vs/base/browser/dom'; import { TPromise } from 'vs/base/common/winjs.base'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Widget } from 'vs/base/browser/ui/widget'; -import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; import Event, { Emitter } from 'vs/base/common/event'; import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -22,7 +21,7 @@ import { ISettingsGroup } from 'vs/workbench/parts/preferences/common/preference import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IAction, Action } from 'vs/base/common/actions'; -import { attachInputBoxStyler, attachStylerCallback, attachCheckboxStyler } from 'vs/platform/theme/common/styler'; +import { attachInputBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { Position } from 'vs/editor/common/core/position'; import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; @@ -32,7 +31,6 @@ import { Separator, ActionBar, ActionsOrientation, BaseActionItem } from 'vs/bas import { MarkdownString } from 'vs/base/common/htmlContent'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IMarginData } from 'vs/editor/browser/controller/mouseTarget'; -import { render as renderOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; import { PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_INACTIVE_TITLE_FOREGROUND } from 'vs/workbench/common/theme'; import { IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; @@ -103,32 +101,20 @@ export class SettingsHeaderWidget extends Widget implements IViewZone { export class DefaultSettingsHeaderWidget extends SettingsHeaderWidget { - private linkElement: HTMLElement; private _onClick = this._register(new Emitter()); public onClick: Event = this._onClick.event; protected create() { super.create(); - this.linkElement = DOM.append(this.titleContainer, DOM.$('a.settings-header-natural-language-link')); - this.linkElement.textContent = localize('defaultSettingsFuzzyPrompt', "Try natural language search!"); - - this.onclick(this.linkElement, e => this._onClick.fire()); this.toggleMessage(true); } - public toggleMessage(hasSettings: boolean, promptFuzzy = false): void { + public toggleMessage(hasSettings: boolean): void { if (hasSettings) { this.setMessage(localize('defaultSettings', "Place your settings in the right hand side editor to override.")); - DOM.addClass(this.linkElement, 'hidden'); } else { this.setMessage(localize('noSettingsFound', "No Settings Found.")); - - if (promptFuzzy) { - DOM.removeClass(this.linkElement, 'hidden'); - } else { - DOM.addClass(this.linkElement, 'hidden'); - } } } } @@ -533,7 +519,6 @@ export class SettingsTargetsWidget extends Widget { export interface SearchOptions extends IInputOptions { focusKey?: IContextKey; - showFuzzyToggle?: boolean; showResultCount?: boolean; } @@ -544,7 +529,6 @@ export class SearchWidget extends Widget { private countElement: HTMLElement; private searchContainer: HTMLElement; private inputBox: InputBox; - private fuzzyToggle: Checkbox; private controlsDiv: HTMLElement; private _onDidChange: Emitter = this._register(new Emitter()); @@ -562,32 +546,10 @@ export class SearchWidget extends Widget { this.create(parent); } - public get fuzzyEnabled(): boolean { - return this.fuzzyToggle.checked && this.fuzzyToggle.enabled; - } - - public set fuzzyEnabled(value: boolean) { - this.fuzzyToggle.checked = value; - } - private create(parent: HTMLElement) { this.domNode = DOM.append(parent, DOM.$('div.settings-header-widget')); this.createSearchContainer(DOM.append(this.domNode, DOM.$('div.settings-search-container'))); this.controlsDiv = DOM.append(this.domNode, DOM.$('div.settings-search-controls')); - if (this.options.showFuzzyToggle) { - this.fuzzyToggle = this._register(new Checkbox({ - actionClassName: 'prefs-natural-language-search-toggle', - isChecked: false, - onChange: () => { - this.inputBox.focus(); - this._onDidChange.fire(); - }, - title: localize('enableFuzzySearch', 'Enable natural language search') - })); - this.fuzzyToggle.domNode.innerHTML = renderOcticons('$(light-bulb)'); - DOM.append(this.controlsDiv, this.fuzzyToggle.domNode); - this._register(attachCheckboxStyler(this.fuzzyToggle, this.themeService)); - } if (this.options.showResultCount) { this.countElement = DOM.append(this.controlsDiv, DOM.$('.settings-count-widget')); @@ -639,16 +601,6 @@ export class SearchWidget extends Widget { } } - public setFuzzyToggleVisible(visible: boolean): void { - if (visible) { - this.fuzzyToggle.domNode.classList.remove('hidden'); - this.fuzzyToggle.enable(); - } else { - this.fuzzyToggle.domNode.classList.add('hidden'); - this.fuzzyToggle.disable(); - } - } - private styleCountElementForeground() { const colorId = DOM.hasClass(this.countElement, 'no-results') ? errorForeground : badgeForeground; const color = this.themeService.getTheme().getColor(colorId); @@ -673,8 +625,7 @@ export class SearchWidget extends Widget { private getControlsWidth(): number { const countWidth = this.countElement ? DOM.getTotalWidth(this.countElement) : 0; - const fuzzyToggleWidth = this.fuzzyToggle ? DOM.getTotalWidth(this.fuzzyToggle.domNode) : 0; - return countWidth + fuzzyToggleWidth + 20; + return countWidth + 20; } public focus() { diff --git a/src/vs/workbench/parts/preferences/common/preferences.ts b/src/vs/workbench/parts/preferences/common/preferences.ts index 77002043556..1c3024c9db1 100644 --- a/src/vs/workbench/parts/preferences/common/preferences.ts +++ b/src/vs/workbench/parts/preferences/common/preferences.ts @@ -56,15 +56,31 @@ export interface ISetting { overrideOf?: ISetting; } +export interface ISearchResult { + filterMatches: ISettingMatch[]; + metadata?: IFilterMetadata; +} + +export interface ISearchResultGroup { + id: string; + label: string; + result: ISearchResult; +} + export interface IFilterResult { - query: string; + query?: string; filteredGroups: ISettingsGroup[]; allGroups: ISettingsGroup[]; matches: IRange[]; - fuzzySearchAvailable?: boolean; metadata?: IFilterMetadata; } +export interface ISettingMatch { + setting: ISetting; + matches: IRange[]; + score: number; +} + export interface IScoredResults { [key: string]: number; } @@ -86,14 +102,14 @@ export interface IPreferencesEditorModel { } export type IGroupFilter = (group: ISettingsGroup) => boolean; -export type ISettingMatcher = (setting: ISetting) => IRange[]; +export type ISettingMatcher = (setting: ISetting) => { matches: IRange[], score: number }; export interface ISettingsEditorModel extends IPreferencesEditorModel { readonly onDidChangeGroups: Event; settingsGroups: ISettingsGroup[]; - groupsTerms: string[]; - filterSettings(filter: string, groupFilter: IGroupFilter, settingMatcher: ISettingMatcher, mostRelevantSettings?: string[]): IFilterResult; + filterSettings(filter: string, groupFilter: IGroupFilter, settingMatcher: ISettingMatcher): ISettingMatch[]; findValueMatches(filter: string, setting: ISetting): IRange[]; + updateResultGroup(id: string, resultGroup: ISearchResultGroup): IFilterResult; } export interface IKeybindingsEditorModel extends IPreferencesEditorModel { @@ -159,15 +175,14 @@ export const IPreferencesSearchService = createDecorator; - startSearch(filter: string, remote: boolean): IPreferencesSearchModel; + getLocalSearchProvider(filter: string): ISearchProvider; + getRemoteSearchProvider(filter: string): ISearchProvider; } -export interface IPreferencesSearchModel { - filterPreferences(preferencesModel: ISettingsEditorModel): TPromise; +export interface ISearchProvider { + searchModel(preferencesModel: ISettingsEditorModel): TPromise; } export const CONTEXT_SETTINGS_EDITOR = new RawContextKey('inSettingsEditor', false); diff --git a/src/vs/workbench/parts/preferences/common/preferencesModels.ts b/src/vs/workbench/parts/preferences/common/preferencesModels.ts index 0ddf3a581cb..70ef306b89c 100644 --- a/src/vs/workbench/parts/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/parts/preferences/common/preferencesModels.ts @@ -5,93 +5,72 @@ import * as nls from 'vs/nls'; import { assign } from 'vs/base/common/objects'; -import { tail } from 'vs/base/common/arrays'; +import * as map from 'vs/base/common/map'; +import { tail, flatten, first } from 'vs/base/common/arrays'; import URI from 'vs/base/common/uri'; import { IReference, Disposable } from 'vs/base/common/lifecycle'; import Event, { Emitter } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; import { visit, JSONVisitor } from 'vs/base/common/json'; -import { ITextModel } from 'vs/editor/common/model'; +import { ITextModel, IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { EditorModel } from 'vs/workbench/common/editor'; import { IConfigurationNode, IConfigurationRegistry, Extensions, OVERRIDE_PROPERTY_PATTERN, IConfigurationPropertySchema, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { ISettingsEditorModel, IKeybindingsEditorModel, ISettingsGroup, ISetting, IFilterResult, ISettingsSection, IGroupFilter, ISettingMatcher } from 'vs/workbench/parts/preferences/common/preferences'; +import { ISettingsEditorModel, IKeybindingsEditorModel, ISettingsGroup, ISetting, IFilterResult, IGroupFilter, ISettingMatcher, ISettingMatch, ISearchResultGroup } from 'vs/workbench/parts/preferences/common/preferences'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { Selection } from 'vs/editor/common/core/selection'; export abstract class AbstractSettingsModel extends EditorModel { - public get groupsTerms(): string[] { - return this.settingsGroups.map(group => '@' + group.id); - } - - protected doFilterSettings(filter: string, groupFilter: IGroupFilter, settingMatcher: ISettingMatcher): IFilterResult { - const allGroups = this.settingsGroups; + protected _currentResultGroups = new Map(); - if (!filter) { - return { - filteredGroups: allGroups, - allGroups, - matches: [], - query: filter - }; + public updateResultGroup(id: string, resultGroup: ISearchResultGroup): IFilterResult { + if (resultGroup) { + this._currentResultGroups.set(id, resultGroup); + } else { + this._currentResultGroups.delete(id); } - const group = this.filterByGroupTerm(filter); - if (group) { - return { - filteredGroups: [group], - allGroups, - matches: [], - query: filter - }; - } + this.removeDuplicateResults(); + return this.update(); + } - const matches: IRange[] = []; - const filteredGroups: ISettingsGroup[] = []; + /** + * Remove duplicates between result groups, preferring results in earlier groups + */ + private removeDuplicateResults(): void { + // Depends on order of map keys + const settingKeys = new Set(); + this._currentResultGroups.forEach((group, id) => { + group.result.filterMatches = group.result.filterMatches.filter(s => !settingKeys.has(s.setting.key)); + group.result.filterMatches.forEach(s => settingKeys.add(s.setting.key)); + }); + } + + public filterSettings(filter: string, groupFilter: IGroupFilter, settingMatcher: ISettingMatcher): ISettingMatch[] { + const allGroups = this.filterGroups; + + const filterMatches: ISettingMatch[] = []; for (const group of allGroups) { const groupMatched = groupFilter(group); - const sections: ISettingsSection[] = []; for (const section of group.sections) { - const settings: ISetting[] = []; for (const setting of section.settings) { - const settingMatches = settingMatcher(setting); - if (groupMatched || settingMatches && settingMatches.length) { - settings.push(setting); - } - - if (settingMatches) { - matches.push(...settingMatches); + const settingMatchResult = settingMatcher(setting); + + if (groupMatched || settingMatchResult) { + filterMatches.push({ + setting, + matches: settingMatchResult && settingMatchResult.matches, + score: settingMatchResult ? settingMatchResult.score : 0 + }); } } - if (settings.length) { - sections.push({ - title: section.title, - settings, - titleRange: section.titleRange - }); - } - } - if (sections.length) { - filteredGroups.push({ - id: group.id, - title: group.title, - titleRange: group.titleRange, - sections, - range: group.range - }); } } - return { filteredGroups, matches, allGroups, query: filter }; - } - private filterByGroupTerm(filter: string): ISettingsGroup { - if (this.groupsTerms.indexOf(filter) !== -1) { - const id = filter.substring(1); - return this.settingsGroups.filter(group => group.id === id)[0]; - } - return null; + return filterMatches.sort((a, b) => b.score - a.score); } public getPreference(key: string): ISetting { @@ -107,9 +86,15 @@ export abstract class AbstractSettingsModel extends EditorModel { return null; } + protected get filterGroups(): ISettingsGroup[] { + return this.settingsGroups; + } + public abstract settingsGroups: ISettingsGroup[]; public abstract findValueMatches(filter: string, setting: ISetting): IRange[]; + + protected abstract update(): IFilterResult; } export class SettingsEditorModel extends AbstractSettingsModel implements ISettingsEditorModel { @@ -149,10 +134,6 @@ export class SettingsEditorModel extends AbstractSettingsModel implements ISetti return this.settingsModel.getValue(); } - public filterSettings(filter: string, groupFilter: IGroupFilter, settingMatcher: ISettingMatcher): IFilterResult { - return this.doFilterSettings(filter, groupFilter, settingMatcher); - } - public findValueMatches(filter: string, setting: ISetting): IRange[] { return this.settingsModel.findMatches(filter, setting.valueRange, false, false, null, false).map(match => match.range); } @@ -164,6 +145,43 @@ export class SettingsEditorModel extends AbstractSettingsModel implements ISetti protected parse(): void { this._settingsGroups = parse(this.settingsModel, (property: string, previousParents: string[]): boolean => this.isSettingsProperty(property, previousParents)); } + + protected update(): IFilterResult { + const resultGroups = map.values(this._currentResultGroups); + if (!resultGroups.length) { + return null; + } + + // Transform resultGroups into IFilterResult - ISetting ranges are already correct here + const filteredSettings: ISetting[] = []; + const matches: IRange[] = []; + resultGroups.forEach(group => { + group.result.filterMatches.forEach(filterMatch => { + filteredSettings.push(filterMatch.setting); + matches.push(...filterMatch.matches); + }); + }); + + let filteredGroup: ISettingsGroup; + const modelGroup = this.settingsGroups[0]; // Editable model has one or zero groups + if (modelGroup) { + filteredGroup = { + id: modelGroup.id, + range: modelGroup.range, + sections: [{ + settings: filteredSettings + }], + title: modelGroup.title, + titleRange: modelGroup.titleRange + }; + } + + return { + allGroups: this.settingsGroups, + filteredGroups: filteredGroup ? [filteredGroup] : [], + matches + }; + } } function parse(model: ITextModel, isSettingsProperty: (currentProperty: string, previousParents: string[]) => boolean): ISettingsGroup[] { @@ -382,6 +400,7 @@ export class DefaultSettings extends Disposable { if (!this._allSettingsGroups) { this.parse(); } + return this._allSettingsGroups; } @@ -390,7 +409,7 @@ export class DefaultSettings extends Disposable { this.initAllSettingsMap(settingsGroups); const mostCommonlyUsed = this.getMostCommonlyUsedSettings(settingsGroups); this._allSettingsGroups = [mostCommonlyUsed, ...settingsGroups]; - this._content = this.toContent(true, [mostCommonlyUsed], settingsGroups); + this._content = this.toContent(true, this._allSettingsGroups); return this._content; } @@ -538,17 +557,14 @@ export class DefaultSettings extends Disposable { return c1.order - c2.order; } - private toContent(asArray: boolean, ...settingsGroups: ISettingsGroup[][]): string { + private toContent(asArray: boolean, settingsGroups: ISettingsGroup[]): string { const builder = new SettingsContentBuilder(); if (asArray) { builder.pushLine('['); } settingsGroups.forEach((settingsGroup, i) => { - builder.pushGroups(settingsGroup); - - if (i !== settingsGroups.length - 1) { - builder.pushLine(','); - } + builder.pushGroup(settingsGroup); + builder.pushLine(','); }); if (asArray) { builder.pushLine(']'); @@ -586,49 +602,101 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements return this.defaultSettings.settingsGroups; } - public filterSettings(filter: string, groupFilter: IGroupFilter, settingMatcher: ISettingMatcher, mostRelevantSettings?: string[]): IFilterResult { - if (mostRelevantSettings) { - const mostRelevantGroup = this.renderMostRelevantSettings(mostRelevantSettings); + protected get filterGroups(): ISettingsGroup[] { + // Don't look at "commonly used" for filter + return this.settingsGroups.slice(1); + } + + protected update(): IFilterResult { + // Grab current result groups, only render non-empty groups + const resultGroups = map.values(this._currentResultGroups); + const nonEmptyResultGroups = resultGroups.filter(group => group.result.filterMatches.length); - // calculate match ranges - const matches = mostRelevantGroup.sections[0].settings.reduce((prev, s) => { - return prev.concat(settingMatcher(s)); - }, []); + const startLine = tail(this.settingsGroups).range.endLineNumber + 2; + const { settingsGroups: filteredGroups, matches } = this.writeResultGroups(nonEmptyResultGroups, startLine); - return { - allGroups: [...this.settingsGroups, mostRelevantGroup], - filteredGroups: mostRelevantGroup.sections[0].settings.length ? [mostRelevantGroup] : [], + const groupWithMetadata = first(resultGroups, group => !!group.result.metadata); + return resultGroups.length ? + { + allGroups: this.settingsGroups, + filteredGroups, matches, - query: filter - }; - } else { - // Do local search and add empty 'most relevant' group - const mostRelevantGroup = this.renderMostRelevantSettings([]); - const result = this.doFilterSettings(filter, groupFilter, settingMatcher); - result.allGroups = [...result.allGroups, mostRelevantGroup]; - return result; - } + metadata: groupWithMetadata && groupWithMetadata.result.metadata + } : + null; } - private renderMostRelevantSettings(mostRelevantSettings: string[]): ISettingsGroup { - const mostRelevantLineOffset = tail(this.settingsGroups).range.endLineNumber + 2; - const builder = new SettingsContentBuilder(mostRelevantLineOffset - 1); + /** + * Translate the ISearchResultGroups to text, and write it to the editor model + */ + private writeResultGroups(groups: ISearchResultGroup[], startLine: number): { matches: IRange[], settingsGroups: ISettingsGroup[] } { + const contentBuilderOffset = startLine - 1; + const builder = new SettingsContentBuilder(contentBuilderOffset); + + const settingsGroups: ISettingsGroup[] = []; + const matches: IRange[] = []; builder.pushLine(','); - const mostRelevantGroup = this.getMostRelevantSettings(mostRelevantSettings); - builder.pushGroups([mostRelevantGroup]); - builder.pushLine(''); + groups.forEach(resultGroup => { + const settingsGroup = this.getGroup(resultGroup); + settingsGroups.push(settingsGroup); + matches.push(...this.writeSettingsGroupToBuilder(builder, settingsGroup, resultGroup.result.filterMatches)); + }); // note: 1-indexed line numbers here - const mostRelevantContent = builder.getContent(); - const mostRelevantEndLine = this._model.getLineCount(); - this._model.applyEdits([ - { - text: mostRelevantContent, - range: new Range(mostRelevantLineOffset, 1, mostRelevantEndLine, 1) - } - ]); + const groupContent = builder.getContent() + '\n'; + const groupEndLine = this._model.getLineCount(); + const cursorPosition = new Selection(startLine, 1, startLine, 1); + const edit: IIdentifiedSingleEditOperation = { + text: groupContent, + forceMoveMarkers: true, + range: new Range(startLine, 1, groupEndLine, 1), + identifier: { major: 1, minor: 0 } + }; + + this._model.pushEditOperations([cursorPosition], [edit], () => [cursorPosition]); + + // Force tokenization now - otherwise it may be slightly delayed, causing a flash of white text + const tokenizeTo = Math.min(startLine + 60, this._model.getLineCount()); + this._model.forceTokenization(tokenizeTo); - return mostRelevantGroup; + return { matches, settingsGroups }; + } + + private writeSettingsGroupToBuilder(builder: SettingsContentBuilder, settingsGroup: ISettingsGroup, filterMatches: ISettingMatch[]): IRange[] { + // Fix match ranges to offset from setting start line + filterMatches = filterMatches.map(filteredMatch => { + return { + setting: filteredMatch.setting, + score: filteredMatch.score, + matches: filteredMatch.matches && filteredMatch.matches.map(match => { + return new Range( + match.startLineNumber - filteredMatch.setting.range.startLineNumber, + match.startColumn, + match.endLineNumber - filteredMatch.setting.range.startLineNumber, + match.endColumn); + }) + }; + }); + + builder.pushGroup(settingsGroup); + builder.pushLine(','); + + // builder has rewritten settings ranges, fix match ranges + const fixedMatches = flatten( + filterMatches + .map(m => m.matches || []) + .map((settingMatches, i) => { + const setting = settingsGroup.sections[0].settings[i]; + return settingMatches.map(range => { + return new Range( + range.startLineNumber + setting.range.startLineNumber, + range.startColumn, + range.endLineNumber + setting.range.startLineNumber, + range.endColumn); + }); + })); + + return fixedMatches; } public findValueMatches(filter: string, setting: ISetting): IRange[] { @@ -648,30 +716,28 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements return null; } - private getMostRelevantSettings(rankedSettingNames: string[]): ISettingsGroup { - const settings = rankedSettingNames.map(key => { - const setting = this.defaultSettings.getSettingByName(key); - if (setting) { - return { - description: setting.description, - key: setting.key, - value: setting.value, - range: null, - valueRange: null, - overrides: [] - }; - } - return null; - }).filter(setting => !!setting); + private copySettings(settings: ISetting[]): ISetting[] { + return settings.map(setting => { + return { + description: setting.description, + key: setting.key, + value: setting.value, + range: null, + valueRange: null, + overrides: [] + }; + }); + } + private getGroup(resultGroup: ISearchResultGroup): ISettingsGroup { return { - id: 'mostRelevant', + id: resultGroup.id, range: null, - title: nls.localize('mostRelevant', "Most Relevant"), + title: resultGroup.label, titleRange: null, sections: [ { - settings + settings: this.copySettings(resultGroup.result.filterMatches.map(m => m.setting)) } ] }; @@ -681,10 +747,6 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements class SettingsContentBuilder { private _contentByLines: string[]; - get lines(): string[] { - return this._contentByLines; - } - private get lineCountWithOffset(): number { return this._contentByLines.length + this._rangeOffset; } @@ -705,24 +767,23 @@ class SettingsContentBuilder { this._contentByLines.push(...lineText); } - pushGroups(settingsGroups: ISettingsGroup[]): void { - let lastSetting: ISetting = null; + pushGroup(settingsGroups: ISettingsGroup): void { this._contentByLines.push('{'); this._contentByLines.push(''); - for (const group of settingsGroups) { - this._contentByLines.push(''); - lastSetting = this.pushGroup(group); - } + this._contentByLines.push(''); + const lastSetting = this._pushGroup(settingsGroups); + if (lastSetting) { // Strip the comma from the last setting const lineIdx = this.offsetIndexToIndex(lastSetting.range.endLineNumber); const content = this._contentByLines[lineIdx - 2]; this._contentByLines[lineIdx - 2] = content.substring(0, content.length - 1); } + this._contentByLines.push('}'); } - private pushGroup(group: ISettingsGroup): ISetting { + private _pushGroup(group: ISettingsGroup): ISetting { const indent = ' '; let lastSetting: ISetting = null; let groupStart = this.lineCountWithOffset + 1; diff --git a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts index f99d62999fd..fb8dac50a05 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts @@ -4,11 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { TPromise } from 'vs/base/common/winjs.base'; -import * as errors from 'vs/base/common/errors'; -import Event, { Emitter } from 'vs/base/common/event'; -import { ISettingsEditorModel, IFilterResult, ISetting, ISettingsGroup, IWorkbenchSettingsConfiguration, IFilterMetadata, IPreferencesSearchService, IPreferencesSearchModel } from 'vs/workbench/parts/preferences/common/preferences'; +import { ISettingsEditorModel, ISetting, ISettingsGroup, IWorkbenchSettingsConfiguration, IFilterMetadata, IPreferencesSearchService, ISearchResult, ISearchProvider, IGroupFilter, ISettingMatcher, IScoredResults } from 'vs/workbench/parts/preferences/common/preferences'; import { IRange } from 'vs/editor/common/core/range'; -import { distinct } from 'vs/base/common/arrays'; +import { distinct, top } from 'vs/base/common/arrays'; import * as strings from 'vs/base/common/strings'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -20,7 +18,6 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IRequestService } from 'vs/platform/request/node/request'; import { asJson } from 'vs/base/node/request'; import { Disposable } from 'vs/base/common/lifecycle'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export interface IEndpointDetails { urlBase: string; @@ -30,19 +27,15 @@ export interface IEndpointDetails { export class PreferencesSearchService extends Disposable implements IPreferencesSearchService { _serviceBrand: any; - private _onRemoteSearchEnablementChanged = new Emitter(); - public onRemoteSearchEnablementChanged: Event = this._onRemoteSearchEnablementChanged.event; - constructor( @IWorkspaceConfigurationService private configurationService: IWorkspaceConfigurationService, @IEnvironmentService private environmentService: IEnvironmentService, @IInstantiationService private instantiationService: IInstantiationService ) { super(); - this._register(configurationService.onDidChangeConfiguration(() => this._onRemoteSearchEnablementChanged.fire(this.remoteSearchAllowed))); } - get remoteSearchAllowed(): boolean { + private get remoteSearchAllowed(): boolean { if (this.environmentService.appQuality === 'stable') { return false; } @@ -69,76 +62,60 @@ export class PreferencesSearchService extends Disposable implements IPreferences } } - startSearch(filter: string, remote: boolean): PreferencesSearchModel { - return this.instantiationService.createInstance(PreferencesSearchModel, this, filter, remote); - } -} - -export class PreferencesSearchModel implements IPreferencesSearchModel { - private _localProvider: LocalSearchProvider; - private _remoteProvider: RemoteSearchProvider; - - constructor( - private provider: IPreferencesSearchService, private filter: string, remote: boolean, - @IInstantiationService instantiationService: IInstantiationService, - @ITelemetryService private telemetryService: ITelemetryService - ) { - this._localProvider = new LocalSearchProvider(filter); - - if (remote && filter) { - this._remoteProvider = instantiationService.createInstance(RemoteSearchProvider, filter, this.provider.endpoint); - } + getRemoteSearchProvider(filter: string): RemoteSearchProvider { + return this.remoteSearchAllowed && this.instantiationService.createInstance(RemoteSearchProvider, filter, this.endpoint); } - filterPreferences(preferencesModel: ISettingsEditorModel): TPromise { - if (!this.filter) { - return TPromise.wrap(null); - } - - if (this._remoteProvider) { - return this._remoteProvider.filterPreferences(preferencesModel).then(null, err => { - const message = errors.getErrorMessage(err); - - if (message.toLowerCase() !== 'canceled') { - /* __GDPR__ - "defaultSettings.searchError" : { - "message": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('defaultSettings.searchError', { message }); - } - - return this._localProvider.filterPreferences(preferencesModel); - }); - } else { - return this._localProvider.filterPreferences(preferencesModel); - } + getLocalSearchProvider(filter: string): LocalSearchProvider { + return this.instantiationService.createInstance(LocalSearchProvider, filter); } } -class LocalSearchProvider { +export class LocalSearchProvider implements ISearchProvider { private _filter: string; constructor(filter: string) { this._filter = filter; } - filterPreferences(preferencesModel: ISettingsEditorModel): TPromise { - const regex = strings.createRegExp(this._filter, false, { global: true }); - - const groupFilter = (group: ISettingsGroup) => { - return regex.test(group.title); - }; + searchModel(preferencesModel: ISettingsEditorModel): TPromise { + if (!this._filter) { + return TPromise.wrap(null); + } + let score = 1000; // Sort is not stable const settingMatcher = (setting: ISetting) => { - return new SettingMatches(this._filter, setting, true, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches; + const matches = new SettingMatches(this._filter, setting, true, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches; + return matches && matches.length ? + { + matches, + score: score-- + } : + null; }; - return TPromise.wrap(preferencesModel.filterSettings(this._filter, groupFilter, settingMatcher)); + const filterMatches = preferencesModel.filterSettings(this._filter, this.getGroupFilter(this._filter), settingMatcher); + return TPromise.wrap({ + filterMatches + }); + } + + private getGroupFilter(filter: string): IGroupFilter { + if (strings.startsWith(filter, '@')) { + const groupId = filter.replace(/^@/, ''); + return (group: ISettingsGroup) => { + return group.id.toLowerCase() === groupId.toLowerCase(); + }; + } else { + const regex = strings.createRegExp(this._filter, false, { global: true }); + return (group: ISettingsGroup) => { + return regex.test(group.title); + }; + } } } -class RemoteSearchProvider { +export class RemoteSearchProvider implements ISearchProvider { private _filter: string; private _remoteSearchP: TPromise; @@ -147,23 +124,26 @@ class RemoteSearchProvider { @IRequestService private requestService: IRequestService ) { this._filter = filter; - this._remoteSearchP = filter ? this.getSettingsFromBing(filter, endpoint) : TPromise.wrap(null); + + // @queries are always handled by local filter + this._remoteSearchP = filter && !strings.startsWith(filter, '@') ? + this.getSettingsFromBing(filter, endpoint) : + TPromise.wrap(null); } - filterPreferences(preferencesModel: ISettingsEditorModel): TPromise { + searchModel(preferencesModel: ISettingsEditorModel): TPromise { return this._remoteSearchP.then(remoteResult => { if (remoteResult) { - let sortedNames = Object.keys(remoteResult.scoredResults).sort((a, b) => remoteResult.scoredResults[b] - remoteResult.scoredResults[a]); - if (sortedNames.length) { - const highScore = remoteResult.scoredResults[sortedNames[0]]; - const minScore = highScore / 5; - sortedNames = sortedNames.filter(name => remoteResult.scoredResults[name] >= minScore); - } - - const settingMatcher = this.getRemoteSettingMatcher(sortedNames, preferencesModel); - const result = preferencesModel.filterSettings(this._filter, group => null, settingMatcher, sortedNames); - result.metadata = remoteResult; - return result; + const highScoreKey = top(Object.keys(remoteResult.scoredResults), (a, b) => remoteResult.scoredResults[b] - remoteResult.scoredResults[a], 1)[0]; + const highScore = highScoreKey ? remoteResult.scoredResults[highScoreKey] : 0; + const minScore = highScore / 5; + + const settingMatcher = this.getRemoteSettingMatcher(remoteResult.scoredResults, minScore, preferencesModel); + const filterMatches = preferencesModel.filterSettings(this._filter, group => null, settingMatcher); + return { + filterMatches, + metadata: remoteResult + }; } else { return null; } @@ -218,19 +198,15 @@ class RemoteSearchProvider { return TPromise.as(p as any); } - private getRemoteSettingMatcher(names: string[], preferencesModel: ISettingsEditorModel): any { - const resultSet = new Set(); - names.forEach(name => resultSet.add(name)); - + private getRemoteSettingMatcher(scoredResults: IScoredResults, minScore: number, preferencesModel: ISettingsEditorModel): ISettingMatcher { return (setting: ISetting) => { - if (resultSet.has(setting.key)) { + const score = scoredResults[setting.key]; + if (typeof score === 'number' && score >= minScore) { const settingMatches = new SettingMatches(this._filter, setting, false, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches; - if (settingMatches.length) { - return settingMatches; - } + return { matches: settingMatches, score: scoredResults[setting.key] }; } - return []; + return null; }; } } -- GitLab