未验证 提交 1aaa1006 编写于 作者: R Rob Lourens 提交者: GitHub

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
上级 38fbf812
......@@ -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<void>;
private filterThrottle: ThrottledDelayer<void>;
private remoteSearchThrottle: ThrottledDelayer<void>;
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<void>(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<void> {
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<void> {
const query = this.searchWidget.getValue().trim();
return this.preferencesRenderers.remoteSearchPreferences(query).then(result => {
this.onSearchResult(query, result);
}, onUnexpectedError);
}
private localFilterPreferences(): TPromise<void> {
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<ISetting> {
}
}
interface ISearchCriteria {
filter: string;
fuzzy: boolean;
interface IFilterOrSearchResult {
count: number;
metadata: IFilterMetadata;
}
class PreferencesRenderers extends Disposable {
class PreferencesRenderersController extends Disposable {
private _defaultPreferencesRenderer: IPreferencesRenderer<ISetting>;
private _defaultPreferencesRendererDisposables: IDisposable[] = [];
private _defaultPreferencesFilterResult: IFilterResult;
private _editablePreferencesFilterResult: IFilterResult;
private _editablePreferencesRenderer: IPreferencesRenderer<ISetting>;
private _editablePreferencesRendererDisposables: IDisposable[] = [];
private _settingsNavigator: SettingsNavigator;
private _filtersInProgress: TPromise<any>[];
private _searchCriteria: ISearchCriteria;
private _currentSearchModel: IPreferencesSearchModel;
private _onTriggeredFuzzy: Emitter<void> = this._register(new Emitter<void>());
public onTriggeredFuzzy: Event<void> = this._onTriggeredFuzzy.event;
private _currentLocalSearchProvider: ISearchProvider;
private _currentRemoteSearchProvider: ISearchProvider;
private _lastQuery: string;
private _onDidFilterResultsCountChange: Emitter<number> = this._register(new Emitter<number>());
public onDidFilterResultsCountChange: Event<number> = 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) {
(<ISettingsEditorModel>this._editablePreferencesRenderer.preferencesModel).onDidChangeGroups(() => {
if (this._currentSearchModel) {
this._filterEditablePreferences()
.then(() => {
const count = this.consolidateAndUpdate();
this._onDidFilterResultsCountChange.fire(count);
});
}
}, this, this._editablePreferencesRendererDisposables);
(<ISettingsEditorModel>this._editablePreferencesRenderer.preferencesModel)
.onDidChangeGroups(this._onEditableContentDidChange, this, this._editablePreferencesRendererDisposables);
}
}
}
filterPreferences(criteria: ISearchCriteria): TPromise<{ count: number, metadata: IFilterMetadata }> {
this._searchCriteria = criteria;
async _onEditableContentDidChange(): TPromise<void> {
await this.localFilterPreferences(this._lastQuery, true);
await this.remoteSearchPreferences(this._lastQuery, true);
}
remoteSearchPreferences(query: string, updateCurrentResults?: boolean): TPromise<IFilterOrSearchResult> {
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<IFilterOrSearchResult> {
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<IFilterOrSearchResult> {
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<IFilterResult>(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<void> {
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<ISetting>, provider: ISearchProvider, groupId: string, groupLabel: string): TPromise<IFilterResult> {
if (preferencesRenderer) {
const model = <ISettingsEditorModel>preferencesRenderer.preferencesModel;
const searchP = provider ? provider.searchModel(model) : TPromise.wrap(null);
return searchP
.then<ISearchResult>(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<void> {
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<ISetting>): ISettingsGroup[] {
return preferencesRenderer ? (<ISettingsEditorModel>preferencesRenderer.preferencesModel).settingsGroups : [];
}
private _filterPreferences(searchCriteria: ISearchCriteria, preferencesRenderer: IPreferencesRenderer<ISetting>, searchModel: IPreferencesSearchModel): TPromise<IFilterResult> {
if (preferencesRenderer && searchCriteria) {
const prefSearchP = searchModel.filterPreferences(<ISettingsEditorModel>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<ISetting>): 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;
}
......
......@@ -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<T> extends IDisposable {
onFocusPreference: Event<T>;
onClearFocusPreference: Event<T>;
onUpdatePreference?: Event<{ key: string, value: any, source: T, index: number }>;
onTriggeredFuzzy?: Event<void>;
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<ISetting> {
......@@ -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<ISetting> = new Emitter<ISetting>();
public readonly onClearFocusPreference: Event<ISetting> = this._onClearFocusPreference.event;
public readonly onTriggeredFuzzy: Event<void>;
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<ISetting> {
......@@ -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<IIndexedSetting>;
private editPreferenceWidgetForCursorPosition: EditPreferenceWidget<IIndexedSetting>;
private editPreferenceWidgetForMouseMove: EditPreferenceWidget<IIndexedSetting>;
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 = <EditPreferenceWidget<IIndexedSetting>>this._register(this.instantiationService.createInstance(EditPreferenceWidget, editor));
this.editPreferenceWidgetForMouseMove = <EditPreferenceWidget<IIndexedSetting>>this._register(this.instantiationService.createInstance(EditPreferenceWidget, editor));
this.toggleEditPreferencesForMouseMoveDelayer = new Delayer<void>(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<ISetting>): 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();
}
}
......
......@@ -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<void>());
public onClick: Event<void> = 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<boolean>;
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<string> = this._register(new Emitter<string>());
......@@ -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() {
......
......@@ -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<T> {
}
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<ISetting> {
readonly onDidChangeGroups: Event<void>;
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<T> extends IPreferencesEditorModel<T> {
......@@ -159,15 +175,14 @@ export const IPreferencesSearchService = createDecorator<IPreferencesSearchServi
export interface IPreferencesSearchService {
_serviceBrand: any;
remoteSearchAllowed: boolean;
endpoint: IEndpointDetails;
onRemoteSearchEnablementChanged: Event<boolean>;
startSearch(filter: string, remote: boolean): IPreferencesSearchModel;
getLocalSearchProvider(filter: string): ISearchProvider;
getRemoteSearchProvider(filter: string): ISearchProvider;
}
export interface IPreferencesSearchModel {
filterPreferences(preferencesModel: ISettingsEditorModel): TPromise<IFilterResult>;
export interface ISearchProvider {
searchModel(preferencesModel: ISettingsEditorModel): TPromise<ISearchResult>;
}
export const CONTEXT_SETTINGS_EDITOR = new RawContextKey<boolean>('inSettingsEditor', false);
......
......@@ -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<string, ISearchResultGroup>();
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<string>();
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 <IFilterResult>{
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 ?
<IFilterResult>{
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 <ISettingMatch>{
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 <ISetting>{
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 <ISetting>{
description: setting.description,
key: setting.key,
value: setting.value,
range: null,
valueRange: null,
overrides: []
};
});
}
private getGroup(resultGroup: ISearchResultGroup): ISettingsGroup {
return <ISettingsGroup>{
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;
......
......@@ -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<boolean>();
public onRemoteSearchEnablementChanged: Event<boolean> = 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<IFilterResult> {
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<IFilterResult> {
const regex = strings.createRegExp(this._filter, false, { global: true });
const groupFilter = (group: ISettingsGroup) => {
return regex.test(group.title);
};
searchModel(preferencesModel: ISettingsEditorModel): TPromise<ISearchResult> {
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<IFilterMetadata>;
......@@ -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<IFilterResult> {
searchModel(preferencesModel: ISettingsEditorModel): TPromise<ISearchResult> {
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 <ISearchResult>{
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;
};
}
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册