diff --git a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index 411b295653f2cb06ce3fafa83c4de8beea5140b0..669ed0f9cb69047274aed6fbb767d46332e5a3fa 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts @@ -29,6 +29,7 @@ import { SettingsEditorModel, DefaultSettingsEditorModel } from 'vs/workbench/pa import { editorContribution } from 'vs/editor/browser/editorBrowserExtensions'; import { ICodeEditor, IEditorContributionCtor } from 'vs/editor/browser/editorBrowser'; import { SearchWidget, SettingsTargetsWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; +import { PreferencesSearchProvider } from 'vs/workbench/parts/preferences/browser/preferencesSearch'; import { ContextKeyExpr, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Command } from 'vs/editor/common/editorCommonExtensions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -56,7 +57,7 @@ import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRe import { attachStylerCallback } from 'vs/platform/theme/common/styler'; import { scrollbarShadow } from 'vs/platform/theme/common/colorRegistry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import Event, { Emitter } from 'vs/base/common/event'; +import Event, { Emitter, debounceEvent } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; export class PreferencesEditorInput extends SideBySideEditorInput { @@ -107,6 +108,7 @@ export class PreferencesEditor extends BaseEditor { private preferencesRenderers: PreferencesRenderers; private delayedFilterLogging: Delayer; + private onInput: Emitter; private latestEmptyFilters: string[] = []; private lastFocusedWidget: SearchWidget | SideBySidePreferencesWidget = null; @@ -125,6 +127,11 @@ 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.onInput = new Emitter(); + + debounceEvent(this.onInput.event, (l, e) => e, 200, /*leading=*/true)(() => { + this.filterPreferences(this.searchWidget.getValue().trim()); + }); } public createEditor(parent: Builder): void { @@ -138,7 +145,7 @@ export class PreferencesEditor extends BaseEditor { placeholder: nls.localize('SearchSettingsWidget.Placeholder', "Search Settings"), focusKey: this.focusSettingsContextKey })); - this._register(this.searchWidget.onDidChange(value => this.filterPreferences(value.trim()))); + this._register(this.searchWidget.onDidChange(value => this.onInput.fire())); this._register(this.searchWidget.onFocus(() => this.lastFocusedWidget = this.searchWidget)); this.lastFocusedWidget = this.searchWidget; @@ -232,7 +239,7 @@ export class PreferencesEditor extends BaseEditor { return this.sideBySidePreferencesWidget.setInput(newInput.details, newInput.master, options).then(({ defaultPreferencesRenderer, editablePreferencesRenderer }) => { this.preferencesRenderers.defaultPreferencesRenderer = defaultPreferencesRenderer; this.preferencesRenderers.editablePreferencesRenderer = editablePreferencesRenderer; - this.filterPreferences(this.searchWidget.getValue()); + this.onInput.fire(); }); } @@ -300,13 +307,14 @@ export class PreferencesEditor extends BaseEditor { } private filterPreferences(filter: string) { - const count = this.preferencesRenderers.filterPreferences(filter); - const message = filter ? this.showSearchResultsMessage(count) : nls.localize('totalSettingsMessage', "Total {0} Settings", count); - this.searchWidget.showMessage(message, count); - if (count === 0) { - this.latestEmptyFilters.push(filter); - } - this.delayedFilterLogging.trigger(() => this.reportFilteringUsed(filter)); + this.preferencesRenderers.filterPreferences(filter).then(count => { + const message = filter ? this.showSearchResultsMessage(count) : nls.localize('totalSettingsMessage', "Total {0} Settings", count); + this.searchWidget.showMessage(message, count); + if (count === 0) { + this.latestEmptyFilters.push(filter); + } + this.delayedFilterLogging.trigger(() => this.reportFilteringUsed(filter)); + }); } private showSearchResultsMessage(count: number): string { @@ -380,6 +388,7 @@ class PreferencesRenderers extends Disposable { private _defaultPreferencesRenderer: IPreferencesRenderer; private _editablePreferencesRenderer: IPreferencesRenderer; private _settingsNavigator: SettingsNavigator; + private _filtersInProgress: TPromise[]; private _disposables: IDisposable[] = []; @@ -405,19 +414,41 @@ class PreferencesRenderers extends Disposable { this._editablePreferencesRenderer = editableSettingsRenderer; } - public filterPreferences(filter: string): number { - const defaultPreferencesFilterResult = this._filterPreferences(filter, this._defaultPreferencesRenderer); - const editablePreferencesFilterResult = this._filterPreferences(filter, this._editablePreferencesRenderer); + public filterPreferences(filter: string): TPromise { + if (this._filtersInProgress) { + // Resolved/rejected promises have no .cancel() + this._filtersInProgress.forEach(p => p.cancel && p.cancel()); + } + + const searchProvider = new PreferencesSearchProvider(filter); + this._filtersInProgress = [ + this._filterPreferences(filter, searchProvider, this._defaultPreferencesRenderer), + this._filterPreferences(filter, searchProvider, this._editablePreferencesRenderer)]; + + return TPromise.join(this._filtersInProgress).then(filterResults => { + this._filtersInProgress = null; + const defaultPreferencesFilterResult = filterResults[0]; + const editablePreferencesFilterResult = filterResults[1]; + + const defaultPreferencesFilteredGroups = defaultPreferencesFilterResult ? defaultPreferencesFilterResult.filteredGroups : this._getAllPreferences(this._defaultPreferencesRenderer); + const editablePreferencesFilteredGroups = editablePreferencesFilterResult ? editablePreferencesFilterResult.filteredGroups : this._getAllPreferences(this._editablePreferencesRenderer); + const consolidatedSettings = this._consolidateSettings(editablePreferencesFilteredGroups, defaultPreferencesFilteredGroups); - const defaultPreferencesFilteredGroups = defaultPreferencesFilterResult ? defaultPreferencesFilterResult.filteredGroups : this._getAllPreferences(this._defaultPreferencesRenderer); - const editablePreferencesFilteredGroups = editablePreferencesFilterResult ? editablePreferencesFilterResult.filteredGroups : this._getAllPreferences(this._editablePreferencesRenderer); - const consolidatedSettings = this._consolidateSettings(editablePreferencesFilteredGroups, defaultPreferencesFilteredGroups); - this._settingsNavigator = new SettingsNavigator(filter ? consolidatedSettings : []); + if (defaultPreferencesFilterResult && defaultPreferencesFilterResult.scores) { + this._settingsNavigator = null; + } else { + this._settingsNavigator = new SettingsNavigator(filter ? consolidatedSettings : []); + } - return consolidatedSettings.length; + return consolidatedSettings.length; + }); } public focusNextPreference(forward: boolean = true) { + if (!this._settingsNavigator) { + return; + } + const setting = forward ? this._settingsNavigator.next() : this._settingsNavigator.previous(); this._focusPreference(setting, this._defaultPreferencesRenderer); this._focusPreference(setting, this._editablePreferencesRenderer); @@ -427,13 +458,19 @@ class PreferencesRenderers extends Disposable { return preferencesRenderer ? (preferencesRenderer.preferencesModel).settingsGroups : []; } - private _filterPreferences(filter: string, preferencesRenderer: IPreferencesRenderer): IFilterResult { - let filterResult = null; + private _filterPreferences(filter: string, searchProvider: PreferencesSearchProvider, preferencesRenderer: IPreferencesRenderer): TPromise { if (preferencesRenderer) { - filterResult = filter ? (preferencesRenderer.preferencesModel).filterSettings(filter) : null; - preferencesRenderer.filterPreferences(filterResult); + const prefSearchP = filter ? + searchProvider.filterPreferences(preferencesRenderer.preferencesModel) : + TPromise.wrap(null); + + return prefSearchP.then(filterResult => { + preferencesRenderer.filterPreferences(filterResult); + return filterResult; + }); } - return filterResult; + + return TPromise.wrap(null); } private _focusPreference(preference: ISetting, preferencesRenderer: IPreferencesRenderer): void { @@ -839,7 +876,7 @@ abstract class AbstractSettingsEditorContribution extends Disposable { this.preferencesRendererCreationPromise.then(preferencesRenderer => { if (preferencesRenderer) { if (preferencesRenderer.associatedPreferencesModel) { - preferencesRenderer.associatedPreferencesModel.dispose(); + this.preferencesService.disownPreferencesEditorModel(preferencesRenderer.associatedPreferencesModel); } preferencesRenderer.dispose(); } diff --git a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts index d74dd56da464827777015787495a2ed737f6e3f5..eb0f3ee9c33bdbe533ffad561b942c0124349683 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts @@ -6,6 +6,7 @@ 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 { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IAction } from 'vs/base/common/actions'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; @@ -19,7 +20,7 @@ import { IPreferencesService, ISettingsGroup, ISetting, IPreferencesEditorModel, import { SettingsEditorModel, DefaultSettingsEditorModel } from 'vs/workbench/parts/preferences/common/preferencesModels'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { IContextMenuService, ContextSubMenu } from 'vs/platform/contextview/browser/contextView'; -import { SettingsGroupTitleWidget, EditPreferenceWidget, SettingsHeaderWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; +import { SettingsGroupTitleWidget, EditPreferenceWidget, SettingsHeaderWidget, FloatingClickWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { RangeHighlightDecorations } from 'vs/workbench/common/editor/rangeDecorations'; import { IConfigurationEditingService, ConfigurationEditingError, ConfigurationEditingErrorCode, ConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditing'; @@ -247,6 +248,8 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR private filteredMatchesRenderer: FilteredMatchesRenderer; private hiddenAreasRenderer: HiddenAreasRenderer; private editSettingActionRenderer: EditSettingRenderer; + private mostRelevantMatchesRenderer: MostRelevantMatchesRenderer; + private feedbackWidgetRenderer: FeedbackWidgetRenderer; private _onUpdatePreference: Emitter<{ key: string, value: any, source: ISetting }> = new Emitter<{ key: string, value: any, source: ISetting }>(); public readonly onUpdatePreference: Event<{ key: string, value: any, source: ISetting }> = this._onUpdatePreference.event; @@ -270,9 +273,14 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR this.settingsGroupTitleRenderer = this._register(instantiationService.createInstance(SettingsGroupTitleRenderer, editor)); this.filteredMatchesRenderer = this._register(instantiationService.createInstance(FilteredMatchesRenderer, editor)); this.editSettingActionRenderer = this._register(instantiationService.createInstance(EditSettingRenderer, editor, preferencesModel, this.settingHighlighter)); + this.mostRelevantMatchesRenderer = this._register(instantiationService.createInstance(MostRelevantMatchesRenderer, editor)); + this.feedbackWidgetRenderer = this._register(instantiationService.createInstance(FeedbackWidgetRenderer, editor)); + this._register(this.editSettingActionRenderer.onUpdateSetting(e => this._onUpdatePreference.fire(e))); - const paranthesisHidingRenderer = this._register(instantiationService.createInstance(StaticContentHidingRenderer, editor, preferencesModel.settingsGroups)); - this.hiddenAreasRenderer = this._register(instantiationService.createInstance(HiddenAreasRenderer, editor, [this.settingsGroupTitleRenderer, this.filteredMatchesRenderer, paranthesisHidingRenderer])); + const parenthesisHidingRenderer = this._register(instantiationService.createInstance(StaticContentHidingRenderer, editor, preferencesModel.settingsGroups)); + + const hiddenAreasProviders = [this.settingsGroupTitleRenderer, this.filteredMatchesRenderer, parenthesisHidingRenderer, this.mostRelevantMatchesRenderer]; + this.hiddenAreasRenderer = this._register(instantiationService.createInstance(HiddenAreasRenderer, editor, hiddenAreasProviders)); this._register(this.settingsGroupTitleRenderer.onHiddenAreasChanged(() => this.hiddenAreasRenderer.render())); } @@ -289,6 +297,8 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR public render() { this.settingsGroupTitleRenderer.render(this.preferencesModel.settingsGroups); this.editSettingActionRenderer.render(this.preferencesModel.settingsGroups, this._associatedPreferencesModel); + this.mostRelevantMatchesRenderer.render(null); + this.feedbackWidgetRenderer.render(null); this.hiddenAreasRenderer.render(); this.settingHighlighter.clear(true); this.settingsGroupTitleRenderer.showGroup(1); @@ -297,20 +307,31 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR public filterPreferences(filterResult: IFilterResult): void { this.filterResult = filterResult; - if (!filterResult) { + if (filterResult) { + if (filterResult.scores) { + this.filteredMatchesRenderer.render(null); + this.settingsGroupTitleRenderer.render(null); + } else { + this.filteredMatchesRenderer.render(filterResult); + this.settingsGroupTitleRenderer.render(filterResult.filteredGroups); + } + + this.mostRelevantMatchesRenderer.render(filterResult); + this.feedbackWidgetRenderer.render(filterResult); + this.settingsHeaderRenderer.render(filterResult.filteredGroups); + this.settingHighlighter.clear(true); + this.editSettingActionRenderer.render(filterResult.filteredGroups, this._associatedPreferencesModel); + } else { this.settingHighlighter.clear(true); this.filteredMatchesRenderer.render(null); + this.mostRelevantMatchesRenderer.render(null); + this.feedbackWidgetRenderer.render(null); this.settingsHeaderRenderer.render(this.preferencesModel.settingsGroups); this.settingsGroupTitleRenderer.render(this.preferencesModel.settingsGroups); this.settingsGroupTitleRenderer.showGroup(1); this.editSettingActionRenderer.render(this.preferencesModel.settingsGroups, this._associatedPreferencesModel); - } else { - this.filteredMatchesRenderer.render(filterResult); - this.settingsHeaderRenderer.render(filterResult.filteredGroups); - this.settingsGroupTitleRenderer.render(filterResult.filteredGroups); - this.settingHighlighter.clear(true); - this.editSettingActionRenderer.render(filterResult.filteredGroups, this._associatedPreferencesModel); } + this.hiddenAreasRenderer.render(); } @@ -377,6 +398,8 @@ export class StaticContentHidingRenderer extends Disposable implements HiddenAre get hiddenAreas(): IRange[] { const model = this.editor.getModel(); + + // Hide extra chars for "search results" and "commonly used" groups return [ { startLineNumber: 1, @@ -384,6 +407,12 @@ export class StaticContentHidingRenderer extends Disposable implements HiddenAre endLineNumber: 2, endColumn: model.getLineMaxColumn(2) }, + { + startLineNumber: DefaultSettingsEditorModel.MOST_RELEVANT_SECTION_LENGTH, + startColumn: model.getLineMinColumn(DefaultSettingsEditorModel.MOST_RELEVANT_SECTION_LENGTH), + endLineNumber: DefaultSettingsEditorModel.MOST_RELEVANT_SECTION_LENGTH + 3, + endColumn: model.getLineMaxColumn(DefaultSettingsEditorModel.MOST_RELEVANT_SECTION_LENGTH + 3) + }, { startLineNumber: this.settingsGroups[0].range.endLineNumber + 1, startColumn: model.getLineMinColumn(this.settingsGroups[0].range.endLineNumber + 1), @@ -446,6 +475,10 @@ export class SettingsGroupTitleRenderer extends Disposable implements HiddenArea public render(settingsGroups: ISettingsGroup[]) { this.disposeWidgets(); + if (!settingsGroups) { + return; + } + this.settingsGroups = settingsGroups.slice(); this.settingsGroupTitleWidgets = []; for (const group of this.settingsGroups.slice().reverse()) { @@ -531,6 +564,195 @@ export class HiddenAreasRenderer extends Disposable { } } +export class MostRelevantMatchesRenderer extends Disposable implements HiddenAreasProvider { + + private static settingsInsertStart = 4; + private static settingsInsertEnd = DefaultSettingsEditorModel.MOST_RELEVANT_SECTION_LENGTH - 1; + private static emptyLines = MostRelevantMatchesRenderer.settingsInsertEnd - MostRelevantMatchesRenderer.settingsInsertStart + 1; + private static bunchOfNewlines = strings.repeat('\n', MostRelevantMatchesRenderer.emptyLines); + private static editId = 'mostRelevantMatchesRenderer'; + + public hiddenAreas: IRange[] = []; + + constructor(private editor: ICodeEditor, + @IInstantiationService private instantiationService: IInstantiationService + ) { + super(); + } + + public render(result: IFilterResult): void { + this.hiddenAreas = []; + if (result && result.matches.length && result.scores) { + const settingsTextEndLine = this.renderResults(result); + + this.hiddenAreas = [{ + startLineNumber: settingsTextEndLine + 1, + startColumn: 0, + endLineNumber: this.editor.getModel().getLineCount(), + endColumn: 0 + }]; + } else { + this.renderSearchResultsSection(); + this.hiddenAreas = [{ + startLineNumber: MostRelevantMatchesRenderer.settingsInsertStart, + startColumn: 0, + endLineNumber: MostRelevantMatchesRenderer.settingsInsertEnd, + endColumn: 0 + }]; + } + } + + private renderResults(result: IFilterResult): number { + this.hiddenAreas = []; + this.editor.updateOptions({ readOnly: false }); + + const relevantRanges = this.getOrderedSettingRanges(result.filteredGroups, result.allGroups, result.scores, this.editor.getModel()); + let totalLines = 0; + const settingsValue = relevantRanges.map(visibleRange => { + const settingLines = (visibleRange.endLineNumber - visibleRange.startLineNumber) + 1; + if (totalLines + settingLines <= MostRelevantMatchesRenderer.emptyLines) { + totalLines += settingLines; + const value = this.editor.getModel().getValueInRange(visibleRange); + return value.replace(/([^,])\n$/, '$1,\n'); // ensure ends in ',' + } else { + // Skip lines that push the total length past 50 + return null; + } + }) + .filter(line => !!line) + .join('\n'); + + const settingsTextStartLine = MostRelevantMatchesRenderer.settingsInsertStart; + const settingsTextEndLine = settingsTextStartLine + totalLines - 1; + this.editor.executeEdits(MostRelevantMatchesRenderer.editId, [{ + text: settingsValue, + forceMoveMarkers: false, + range: new Range(settingsTextStartLine, 0, settingsTextEndLine, 0), + identifier: { major: 1, minor: 0 } + }]); + + this.editor.updateOptions({ readOnly: true }); + + return settingsTextEndLine; + } + + private renderSearchResultsSection(): void { + this.editor.updateOptions({ readOnly: false }); + + this.editor.executeEdits(MostRelevantMatchesRenderer.editId, [{ + text: MostRelevantMatchesRenderer.bunchOfNewlines, + forceMoveMarkers: false, + range: new Range(MostRelevantMatchesRenderer.settingsInsertStart, 0, MostRelevantMatchesRenderer.settingsInsertEnd + 1, 0), + identifier: { major: 1, minor: 0 } + }]); + + this.editor.updateOptions({ readOnly: true }); + } + + private getOrderedSettingRanges(filteredGroups: ISettingsGroup[], allSettingsGroups: ISettingsGroup[], scores: any, model: editorCommon.IModel): IRange[] { + const matchingRanges: { range: IRange, name: string }[] = []; + for (const group of allSettingsGroups) { + const filteredGroup = filteredGroups.filter(g => g.title === group.title)[0]; + if (filteredGroup) { + for (const section of group.sections) { + for (const setting of section.settings) { + if (this.containsLine(setting.range.startLineNumber, filteredGroup)) { + matchingRanges.push({ + name: setting.key, + range: this.createCompleteRange(setting.range, model) + }); + } + } + } + } + } + + return matchingRanges + .sort((a, b) => scores[b.name] - scores[a.name]) + .map(r => r.range); + } + + 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: editorCommon.IModel): IRange { + return { + startLineNumber: range.startLineNumber, + startColumn: model.getLineMinColumn(range.startLineNumber), + endLineNumber: range.endLineNumber, + endColumn: model.getLineMaxColumn(range.endLineNumber) + }; + } +} + +export class FeedbackWidgetRenderer extends Disposable { + private _feedbackWidget: FloatingClickWidget; + + constructor(private editor: ICodeEditor, + @IInstantiationService private instantiationService: IInstantiationService, + @IWorkbenchEditorService private editorService: IWorkbenchEditorService + ) { + super(); + } + + public render(result: IFilterResult): void { + if (result && result.scores) { + this.showWidget(); + } else if (this._feedbackWidget) { + this.disposeWidget(); + } + } + + private showWidget(): void { + if (!this._feedbackWidget) { + this._feedbackWidget = this._register(this.instantiationService.createInstance(FloatingClickWidget, this.editor, 'Provide feedback', null)); + this._register(this._feedbackWidget.onClick(() => this.getFeedback())); + this._feedbackWidget.render(); + } + } + + private getFeedback(): void { + this.editorService.openEditor({ contents: 'test' }, true).then(feedbackEditor => { + const sendFeedbackWidget = this._register(this.instantiationService.createInstance(FloatingClickWidget, feedbackEditor.getControl(), 'Send feedback', null)); + this._register(sendFeedbackWidget.onClick(() => this.sendFeedback())); + sendFeedbackWidget.render(); + }); + } + + private sendFeedback(): void { + + } + + private disposeWidget(): void { + if (this._feedbackWidget) { + this._feedbackWidget.dispose(); + this._feedbackWidget = null; + } + } + + public dispose() { + this.disposeWidget(); + + super.dispose(); + } +} + export class FilteredMatchesRenderer extends Disposable implements HiddenAreasProvider { private decorationIds: string[] = []; diff --git a/src/vs/workbench/parts/preferences/browser/preferencesSearch.ts b/src/vs/workbench/parts/preferences/browser/preferencesSearch.ts new file mode 100644 index 0000000000000000000000000000000000000000..508c03ec78ee9953761056fc7834ddd9c49f20ae --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/preferencesSearch.ts @@ -0,0 +1,264 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TPromise } from 'vs/base/common/winjs.base'; +import { ISettingsEditorModel, IFilterResult, ISetting, ISettingsGroup } from 'vs/workbench/parts/preferences/common/preferences'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { distinct } 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'; +import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { IMatch, or, matchesContiguousSubString, matchesPrefix, matchesCamelCase, matchesWords } from 'vs/base/common/filters'; + +class SettingMatches { + + private readonly descriptionMatchingWords: Map = new Map(); + private readonly keyMatchingWords: Map = new Map(); + private readonly valueMatchingWords: Map = new Map(); + + public readonly matches: IRange[]; + + constructor(searchString: string, setting: ISetting, private valuesMatcher: (filter: string, setting: ISetting) => IRange[]) { + this.matches = distinct(this._findMatchesInSetting(searchString, setting), (match) => `${match.startLineNumber}_${match.startColumn}_${match.endLineNumber}_${match.endColumn}_`); + } + + private _findMatchesInSetting(searchString: string, setting: ISetting): IRange[] { + const result = this._doFindMatchesInSetting(searchString, setting); + if (setting.overrides && setting.overrides.length) { + for (const subSetting of setting.overrides) { + const subSettingMatches = new SettingMatches(searchString, subSetting, this.valuesMatcher); + let words = searchString.split(' '); + const descriptionRanges: IRange[] = this.getRangesForWords(words, this.descriptionMatchingWords, [subSettingMatches.descriptionMatchingWords, subSettingMatches.keyMatchingWords, subSettingMatches.valueMatchingWords]); + const keyRanges: IRange[] = this.getRangesForWords(words, this.keyMatchingWords, [subSettingMatches.descriptionMatchingWords, subSettingMatches.keyMatchingWords, subSettingMatches.valueMatchingWords]); + const subSettingKeyRanges: IRange[] = this.getRangesForWords(words, subSettingMatches.keyMatchingWords, [this.descriptionMatchingWords, this.keyMatchingWords, subSettingMatches.valueMatchingWords]); + const subSettinValueRanges: IRange[] = this.getRangesForWords(words, subSettingMatches.valueMatchingWords, [this.descriptionMatchingWords, this.keyMatchingWords, subSettingMatches.keyMatchingWords]); + result.push(...descriptionRanges, ...keyRanges, ...subSettingKeyRanges, ...subSettinValueRanges); + result.push(...subSettingMatches.matches); + } + } + return result; + } + + private _doFindMatchesInSetting(searchString: string, setting: ISetting): IRange[] { + const registry: { [qualifiedKey: string]: IJSONSchema } = Registry.as(Extensions.Configuration).getConfigurationProperties(); + const schema: IJSONSchema = registry[setting.key]; + + let words = searchString.split(' '); + const settingKeyAsWords: string = setting.key.split('.').join(' '); + + for (const word of words) { + for (let lineIndex = 0; lineIndex < setting.description.length; lineIndex++) { + const descriptionMatches = matchesWords(word, setting.description[lineIndex], true); + if (descriptionMatches) { + this.descriptionMatchingWords.set(word, descriptionMatches.map(match => this.toDescriptionRange(setting, match, lineIndex))); + } + } + + const keyMatches = or(matchesWords, matchesCamelCase)(word, settingKeyAsWords); + if (keyMatches) { + this.keyMatchingWords.set(word, keyMatches.map(match => this.toKeyRange(setting, match))); + } + + const valueMatches = typeof setting.value === 'string' ? matchesContiguousSubString(word, setting.value) : null; + if (valueMatches) { + this.valueMatchingWords.set(word, valueMatches.map(match => this.toValueRange(setting, match))); + } else if (schema && schema.enum && schema.enum.some(enumValue => typeof enumValue === 'string' && !!matchesContiguousSubString(word, enumValue))) { + this.valueMatchingWords.set(word, []); + } + } + + const descriptionRanges: IRange[] = []; + for (let lineIndex = 0; lineIndex < setting.description.length; lineIndex++) { + const matches = or(matchesContiguousSubString)(searchString, setting.description[lineIndex] || '') || []; + descriptionRanges.push(...matches.map(match => this.toDescriptionRange(setting, match, lineIndex))); + } + if (descriptionRanges.length === 0) { + descriptionRanges.push(...this.getRangesForWords(words, this.descriptionMatchingWords, [this.keyMatchingWords, this.valueMatchingWords])); + } + + const keyMatches = or(matchesPrefix, matchesContiguousSubString)(searchString, setting.key); + const keyRanges: IRange[] = keyMatches ? keyMatches.map(match => this.toKeyRange(setting, match)) : this.getRangesForWords(words, this.keyMatchingWords, [this.descriptionMatchingWords, this.valueMatchingWords]); + + let valueRanges: IRange[] = []; + if (setting.value && typeof setting.value === 'string') { + const valueMatches = or(matchesPrefix, matchesContiguousSubString)(searchString, setting.value); + valueRanges = valueMatches ? valueMatches.map(match => this.toValueRange(setting, match)) : this.getRangesForWords(words, this.valueMatchingWords, [this.keyMatchingWords, this.descriptionMatchingWords]); + } else { + valueRanges = this.valuesMatcher(searchString, setting); + } + + return [...descriptionRanges, ...keyRanges, ...valueRanges]; + } + + private getRangesForWords(words: string[], from: Map, others: Map[]): IRange[] { + const result: IRange[] = []; + for (const word of words) { + const ranges = from.get(word); + if (ranges) { + result.push(...ranges); + } else if (others.every(o => !o.has(word))) { + return []; + } + } + return result; + } + + private toKeyRange(setting: ISetting, match: IMatch): IRange { + return { + startLineNumber: setting.keyRange.startLineNumber, + startColumn: setting.keyRange.startColumn + match.start, + endLineNumber: setting.keyRange.startLineNumber, + endColumn: setting.keyRange.startColumn + match.end + }; + } + + private toDescriptionRange(setting: ISetting, match: IMatch, lineIndex: number): IRange { + return { + startLineNumber: setting.descriptionRanges[lineIndex].startLineNumber, + startColumn: setting.descriptionRanges[lineIndex].startColumn + match.start, + endLineNumber: setting.descriptionRanges[lineIndex].endLineNumber, + endColumn: setting.descriptionRanges[lineIndex].startColumn + match.end + }; + } + + private toValueRange(setting: ISetting, match: IMatch): IRange { + return { + startLineNumber: setting.valueRange.startLineNumber, + startColumn: setting.valueRange.startColumn + match.start + 1, + endLineNumber: setting.valueRange.startLineNumber, + endColumn: setting.valueRange.startColumn + match.end + 1 + }; + } +} + +export class PreferencesSearchProvider { + private _localProvider: LocalSearchProvider; + private _remoteProvider: RemoteSearchProvider; + + constructor(filter: string) { + this._localProvider = new LocalSearchProvider(filter); + this._remoteProvider = new RemoteSearchProvider(filter); + } + + filterPreferences(preferencesModel: ISettingsEditorModel): TPromise { + return this._remoteProvider.filterPreferences(preferencesModel).then(null, err => { + return this._localProvider.filterPreferences(preferencesModel); + }); + } +} + +class LocalSearchProvider { + 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); + }; + + const settingFilter = (setting: ISetting) => { + return new SettingMatches(this._filter, setting, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches; + }; + + return TPromise.wrap(preferencesModel.filterSettings(this._filter, groupFilter, settingFilter)); + } +} + +class RemoteSearchProvider { + private _filter: string; + private _remoteSearchP: TPromise<{ [key: string]: number }>; + + constructor(filter: string) { + this._filter = filter; + this._remoteSearchP = filter ? getSettingsFromBing(filter) : TPromise.wrap({}); + } + + filterPreferences(preferencesModel: ISettingsEditorModel): TPromise { + return this._remoteSearchP.then(settingsSet => { + const settingFilter = (setting: ISetting) => { + if (!!settingsSet[setting.key]) { + const settingMatches = new SettingMatches(this._filter, setting, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches; + if (settingMatches.length) { + return settingMatches; + } else { + return [new Range(setting.keyRange.startLineNumber, setting.keyRange.startColumn, setting.keyRange.endLineNumber, setting.keyRange.startColumn)]; + } + } else { + return null; + } + }; + + const result = preferencesModel.filterSettings(this._filter, group => null, settingFilter); + result.scores = settingsSet; + return result; + }); + } +} + +function getSettingsFromBing(filter: string): TPromise<{ [key: string]: number }> { + const url = prepareUrl(filter); + console.log('fetching: ' + url); + const start = Date.now(); + const p = fetch(url, { + headers: { + 'User-Agent': 'request', + 'Content-Type': 'application/json; charset=utf-8', + 'api-key': endpoint.key + } + }) + .then(r => r.json()) + .then(result => { + console.log('time: ' + (Date.now() - start) / 1000); + const suggestions = (result.value || []) + .map(r => ({ + name: r.Setting, + score: r['@search.score'] + })); + + const suggSet = Object.create(null); + suggestions.forEach(s => { + const name = s.name + .replace(/^"/, '') + .replace(/"$/, ''); + suggSet[name] = s.score; + }); + + return suggSet; + }); + + return TPromise.as(p as any); +} + +const endpoint = { + key: 'F3F22B32DD89DDA74B1935ED0BE6FCBA', + urlBase: 'https://vscodesearch6.search.windows.net/indexes/vscodeindex/docs' +}; + +const API_VERSION = 'api-version=2015-02-28-Preview'; +const QUERY_TYPE = 'querytype=full'; +const SCORING_PROFILE = 'scoringProfile=ranking1'; + +function escapeSpecialChars(query: string): string { + return query.replace(/\./g, ' ') + .replace(/[\\/+\-&|!"~*?:(){}\[\]\^]/g, '\\$&') + .replace(/ /g, ' ') // collapse spaces + .trim(); +} + +function prepareUrl(query: string): string { + query = escapeSpecialChars(query); + const userQuery = query; + + // Appending Fuzzy after each word. + query = query.replace(/\ +/g, '~ ') + '~'; + + return `${endpoint.urlBase}?${API_VERSION}&search=${encodeURIComponent(userQuery + ' || ' + query)}&${QUERY_TYPE}&${SCORING_PROFILE}`; +} diff --git a/src/vs/workbench/parts/preferences/browser/preferencesService.ts b/src/vs/workbench/parts/preferences/browser/preferencesService.ts index 5e5597721c648f990da520c5e99b71e048841000..085b58bfb283c1e04028984ad80385ddc33c4ad3 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesService.ts @@ -59,10 +59,14 @@ export class PreferencesService extends Disposable implements IPreferencesServic // TODO:@sandy merge these models into editor inputs by extending resource editor model private defaultPreferencesEditorModels: ResourceMap>>; + private defaultPreferencesEditorModelsInUse: ResourceMap; private lastOpenedSettingsInput: PreferencesEditorInput = null; private _onDispose: Emitter = new Emitter(); + private _defaultSettingsUriCounter = 0; + private _defaultResourceSettingsUriCounter = 0; + constructor( @IWorkbenchEditorService private editorService: IWorkbenchEditorService, @IEditorGroupService private editorGroupService: IEditorGroupService, @@ -84,6 +88,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic ) { super(); this.defaultPreferencesEditorModels = new ResourceMap>>(); + this.defaultPreferencesEditorModelsInUse = new ResourceMap(); this.editorGroupService.onEditorsChanged(() => { const activeEditorInput = this.editorService.getActiveEditorInput(); if (activeEditorInput instanceof PreferencesEditorInput) { @@ -103,8 +108,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic }); } - readonly defaultSettingsResource = URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: '/settings.json' }); - readonly defaultResourceSettingsResource = URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: '/resourceSettings.json' }); + readonly defaultSettingsResource = URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: '/0/settings.json' }); + readonly defaultResourceSettingsResource = URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: '/0/resourceSettings.json' }); readonly defaultKeybindingsResource = URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: '/keybindings.json' }); private readonly workspaceConfigSettingsResource = URI.from({ scheme: network.Schemas.vscode, authority: 'settings', path: '/workspaceSettings.json' }); @@ -129,13 +134,28 @@ export class PreferencesService extends Disposable implements IPreferencesServic .then(preferencesEditorModel => preferencesEditorModel ? preferencesEditorModel.content : null); } + disownPreferencesEditorModel(editorModel: IPreferencesEditorModel): void { + const uriStr = editorModel.uri.toString(); + if (uriStr === this.defaultSettingsResource.toString()) { + this.defaultPreferencesEditorModelsInUse.set(editorModel.uri, false); + } else { + this.defaultPreferencesEditorModels.delete(editorModel.uri); + editorModel.dispose(); + } + } + createPreferencesEditorModel(uri: URI): TPromise> { + // Mark model 0 in use + if (uri.toString() === this.defaultSettingsResource.toString()) { + this.defaultPreferencesEditorModelsInUse.set(uri, true); + } + let promise = this.defaultPreferencesEditorModels.get(uri); if (promise) { return promise; } - if (this.defaultSettingsResource.toString() === uri.toString()) { + if (this.isDefaultSettingsResource(uri)) { promise = TPromise.join([this.extensionService.onReady(), this.fetchMostCommonlyUsedSettings()]) .then(result => { const mostCommonSettings = result[1]; @@ -146,7 +166,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic return promise; } - if (this.defaultResourceSettingsResource.toString() === uri.toString()) { + if (this.isDefaultResourceSettingsResource(uri)) { promise = TPromise.join([this.extensionService.onReady(), this.fetchMostCommonlyUsedSettings()]) .then(result => { const mostCommonSettings = result[1]; @@ -273,11 +293,28 @@ export class PreferencesService extends Disposable implements IPreferencesServic }); } + private isDefaultSettingsResource(uri: URI): boolean { + return uri.authority === 'defaultsettings' && uri.scheme === network.Schemas.vscode && !!uri.path.match(/\/\d+\/settings\.json$/); + } + + private isDefaultResourceSettingsResource(uri: URI): boolean { + return uri.authority === 'defaultsettings' && uri.scheme === network.Schemas.vscode && !!uri.path.match(/\/\d+\/resourceSettings\.json$/); + } + private getDefaultSettingsResource(configurationTarget: ConfigurationTarget): URI { if (configurationTarget === ConfigurationTarget.FOLDER) { - return this.defaultResourceSettingsResource; + if (this.defaultPreferencesEditorModelsInUse.get(this.defaultSettingsResource)) { + return URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: `/${this._defaultResourceSettingsUriCounter++}/resourceSettings.json` }); + } else { + return this.defaultResourceSettingsResource; + } + } else { + if (this.defaultPreferencesEditorModelsInUse.get(this.defaultSettingsResource)) { + return URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: `/${this._defaultSettingsUriCounter++}/settings.json` }); + } else { + return this.defaultSettingsResource; + } } - return this.defaultSettingsResource; } private getPreferencesEditorInputName(target: ConfigurationTarget, resource: URI): string { diff --git a/src/vs/workbench/parts/preferences/common/preferences.ts b/src/vs/workbench/parts/preferences/common/preferences.ts index 1b7200927c9749a7ab107d44a3467d33ae86b786..d4faa26078e5cc517f1385635d2c03a9282dff43 100644 --- a/src/vs/workbench/parts/preferences/common/preferences.ts +++ b/src/vs/workbench/parts/preferences/common/preferences.ts @@ -44,6 +44,7 @@ export interface IFilterResult { filteredGroups: ISettingsGroup[]; allGroups: ISettingsGroup[]; matches: IRange[]; + scores?: { [key: string]: number }; } export interface IPreferencesEditorModel { @@ -53,10 +54,14 @@ export interface IPreferencesEditorModel { dispose(): void; } +export type IGroupFilter = (group: ISettingsGroup) => boolean; +export type ISettingFilter = (setting: ISetting) => IRange[]; + export interface ISettingsEditorModel extends IPreferencesEditorModel { settingsGroups: ISettingsGroup[]; groupsTerms: string[]; - filterSettings(filter: string): IFilterResult; + filterSettings(filter: string, groupFilter: IGroupFilter, settingFilter: ISettingFilter): IFilterResult; + findValueMatches(filter: string, setting: ISetting): IRange[]; } export interface IKeybindingsEditorModel extends IPreferencesEditorModel { @@ -68,13 +73,13 @@ export interface IPreferencesService { _serviceBrand: any; defaultSettingsResource: URI; - defaultResourceSettingsResource: URI; userSettingsResource: URI; workspaceSettingsResource: URI; getFolderSettingsResource(resource: URI): URI; resolveContent(uri: URI): TPromise; createPreferencesEditorModel(uri: URI): TPromise>; + disownPreferencesEditorModel(editorModel: IPreferencesEditorModel): void; openGlobalSettings(): TPromise; openWorkspaceSettings(): TPromise; diff --git a/src/vs/workbench/parts/preferences/common/preferencesModels.ts b/src/vs/workbench/parts/preferences/common/preferencesModels.ts index f17231a589ba0de039bc272d428ab39544349277..abae893c507da867823bbcc6c5cfa72a30ecf0c2 100644 --- a/src/vs/workbench/parts/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/parts/preferences/common/preferencesModels.ts @@ -4,22 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as strings from 'vs/base/common/strings'; import { assign } from 'vs/base/common/objects'; -import { distinct } from 'vs/base/common/arrays'; +import * as arrays from 'vs/base/common/arrays'; import URI from 'vs/base/common/uri'; import { IReference } from 'vs/base/common/lifecycle'; import Event from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; import { visit, JSONVisitor } from 'vs/base/common/json'; import { IModel } from 'vs/editor/common/editorCommon'; -import { IJSONSchema } from 'vs/base/common/jsonSchema'; 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 } from 'vs/workbench/parts/preferences/common/preferences'; +import { ISettingsEditorModel, IKeybindingsEditorModel, ISettingsGroup, ISetting, IFilterResult, ISettingsSection, IGroupFilter, ISettingFilter } from 'vs/workbench/parts/preferences/common/preferences'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditing'; -import { IMatch, or, matchesContiguousSubString, matchesPrefix, matchesCamelCase, matchesWords } from 'vs/base/common/filters'; import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { IRange } from 'vs/editor/common/core/range'; import { ITextFileService, StateChange } from 'vs/workbench/services/textfile/common/textfiles'; @@ -27,135 +24,15 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { Queue } from 'vs/base/common/async'; import { IFileService } from 'vs/platform/files/common/files'; -class SettingMatches { - - private readonly descriptionMatchingWords: Map = new Map(); - private readonly keyMatchingWords: Map = new Map(); - private readonly valueMatchingWords: Map = new Map(); - - public readonly matches: IRange[]; - - constructor(searchString: string, setting: ISetting, private valuesMatcher: (filter: string, setting: ISetting) => IRange[]) { - this.matches = distinct(this._findMatchesInSetting(searchString, setting), (match) => `${match.startLineNumber}_${match.startColumn}_${match.endLineNumber}_${match.endColumn}_`); - } - - private _findMatchesInSetting(searchString: string, setting: ISetting): IRange[] { - const result = this._doFindMatchesInSetting(searchString, setting); - if (setting.overrides && setting.overrides.length) { - for (const subSetting of setting.overrides) { - const subSettingMatches = new SettingMatches(searchString, subSetting, this.valuesMatcher); - let words = searchString.split(' '); - const descriptionRanges: IRange[] = this.getRangesForWords(words, this.descriptionMatchingWords, [subSettingMatches.descriptionMatchingWords, subSettingMatches.keyMatchingWords, subSettingMatches.valueMatchingWords]); - const keyRanges: IRange[] = this.getRangesForWords(words, this.keyMatchingWords, [subSettingMatches.descriptionMatchingWords, subSettingMatches.keyMatchingWords, subSettingMatches.valueMatchingWords]); - const subSettingKeyRanges: IRange[] = this.getRangesForWords(words, subSettingMatches.keyMatchingWords, [this.descriptionMatchingWords, this.keyMatchingWords, subSettingMatches.valueMatchingWords]); - const subSettinValueRanges: IRange[] = this.getRangesForWords(words, subSettingMatches.valueMatchingWords, [this.descriptionMatchingWords, this.keyMatchingWords, subSettingMatches.keyMatchingWords]); - result.push(...descriptionRanges, ...keyRanges, ...subSettingKeyRanges, ...subSettinValueRanges); - result.push(...subSettingMatches.matches); - } - } - return result; - } - - private _doFindMatchesInSetting(searchString: string, setting: ISetting): IRange[] { - const registry: { [qualifiedKey: string]: IJSONSchema } = Registry.as(Extensions.Configuration).getConfigurationProperties(); - const schema: IJSONSchema = registry[setting.key]; - - let words = searchString.split(' '); - const settingKeyAsWords: string = setting.key.split('.').join(' '); - - for (const word of words) { - for (let lineIndex = 0; lineIndex < setting.description.length; lineIndex++) { - const descriptionMatches = matchesWords(word, setting.description[lineIndex], true); - if (descriptionMatches) { - this.descriptionMatchingWords.set(word, descriptionMatches.map(match => this.toDescriptionRange(setting, match, lineIndex))); - } - } - - const keyMatches = or(matchesWords, matchesCamelCase)(word, settingKeyAsWords); - if (keyMatches) { - this.keyMatchingWords.set(word, keyMatches.map(match => this.toKeyRange(setting, match))); - } - - const valueMatches = typeof setting.value === 'string' ? matchesContiguousSubString(word, setting.value) : null; - if (valueMatches) { - this.valueMatchingWords.set(word, valueMatches.map(match => this.toValueRange(setting, match))); - } else if (schema && schema.enum && schema.enum.some(enumValue => typeof enumValue === 'string' && !!matchesContiguousSubString(word, enumValue))) { - this.valueMatchingWords.set(word, []); - } - } - - const descriptionRanges: IRange[] = []; - for (let lineIndex = 0; lineIndex < setting.description.length; lineIndex++) { - const matches = or(matchesContiguousSubString)(searchString, setting.description[lineIndex] || '') || []; - descriptionRanges.push(...matches.map(match => this.toDescriptionRange(setting, match, lineIndex))); - } - if (descriptionRanges.length === 0) { - descriptionRanges.push(...this.getRangesForWords(words, this.descriptionMatchingWords, [this.keyMatchingWords, this.valueMatchingWords])); - } - - const keyMatches = or(matchesPrefix, matchesContiguousSubString)(searchString, setting.key); - const keyRanges: IRange[] = keyMatches ? keyMatches.map(match => this.toKeyRange(setting, match)) : this.getRangesForWords(words, this.keyMatchingWords, [this.descriptionMatchingWords, this.valueMatchingWords]); - - let valueRanges: IRange[] = []; - if (setting.value && typeof setting.value === 'string') { - const valueMatches = or(matchesPrefix, matchesContiguousSubString)(searchString, setting.value); - valueRanges = valueMatches ? valueMatches.map(match => this.toValueRange(setting, match)) : this.getRangesForWords(words, this.valueMatchingWords, [this.keyMatchingWords, this.descriptionMatchingWords]); - } else { - valueRanges = this.valuesMatcher(searchString, setting); - } - - return [...descriptionRanges, ...keyRanges, ...valueRanges]; - } - - private getRangesForWords(words: string[], from: Map, others: Map[]): IRange[] { - const result: IRange[] = []; - for (const word of words) { - const ranges = from.get(word); - if (ranges) { - result.push(...ranges); - } else if (others.every(o => !o.has(word))) { - return []; - } - } - return result; - } - - private toKeyRange(setting: ISetting, match: IMatch): IRange { - return { - startLineNumber: setting.keyRange.startLineNumber, - startColumn: setting.keyRange.startColumn + match.start, - endLineNumber: setting.keyRange.startLineNumber, - endColumn: setting.keyRange.startColumn + match.end - }; - } - - private toDescriptionRange(setting: ISetting, match: IMatch, lineIndex: number): IRange { - return { - startLineNumber: setting.descriptionRanges[lineIndex].startLineNumber, - startColumn: setting.descriptionRanges[lineIndex].startColumn + match.start, - endLineNumber: setting.descriptionRanges[lineIndex].endLineNumber, - endColumn: setting.descriptionRanges[lineIndex].startColumn + match.end - }; - } - - private toValueRange(setting: ISetting, match: IMatch): IRange { - return { - startLineNumber: setting.valueRange.startLineNumber, - startColumn: setting.valueRange.startColumn + match.start + 1, - endLineNumber: setting.valueRange.startLineNumber, - endColumn: setting.valueRange.startColumn + match.end + 1 - }; - } -} - - export abstract class AbstractSettingsModel extends EditorModel { public get groupsTerms(): string[] { return this.settingsGroups.map(group => '@' + group.id); } - protected doFilterSettings(filter: string, allGroups: ISettingsGroup[]): IFilterResult { + protected doFilterSettings(filter: string, groupFilter: IGroupFilter, settingFilter: ISettingFilter): IFilterResult { + const allGroups = this.settingsGroups; + if (!filter) { return { filteredGroups: allGroups, @@ -175,18 +52,20 @@ export abstract class AbstractSettingsModel extends EditorModel { const matches: IRange[] = []; const filteredGroups: ISettingsGroup[] = []; - const regex = strings.createRegExp(filter, false, { global: true }); for (const group of allGroups) { - const groupMatched = regex.test(group.title); + const groupMatched = groupFilter(group); const sections: ISettingsSection[] = []; for (const section of group.sections) { const settings: ISetting[] = []; for (const setting of section.settings) { - const settingMatches = new SettingMatches(filter, setting, (filter, setting) => this.findValueMatches(filter, setting)).matches; - if (groupMatched || settingMatches.length > 0) { + const settingMatches = settingFilter(setting); + if (groupMatched || settingMatches && settingMatches.length) { settings.push(setting); } - matches.push(...settingMatches); + + if (settingMatches) { + matches.push(...settingMatches); + } } if (settings.length) { sections.push({ @@ -232,7 +111,7 @@ export abstract class AbstractSettingsModel extends EditorModel { public abstract settingsGroups: ISettingsGroup[]; - protected abstract findValueMatches(filter: string, setting: ISetting): IRange[]; + public abstract findValueMatches(filter: string, setting: ISetting): IRange[]; } export class SettingsEditorModel extends AbstractSettingsModel implements ISettingsEditorModel { @@ -270,8 +149,12 @@ export class SettingsEditorModel extends AbstractSettingsModel implements ISetti return this.settingsModel.getValue(); } - public filterSettings(filter: string): IFilterResult { - return this.doFilterSettings(filter, this.settingsGroups); + public filterSettings(filter: string, groupFilter: IGroupFilter, settingFilter: ISettingFilter): IFilterResult { + return this.doFilterSettings(filter, groupFilter, settingFilter); + } + + public findValueMatches(filter: string, setting: ISetting): IRange[] { + return this.settingsModel.findMatches(filter, setting.valueRange, false, false, null, false).map(match => match.range); } public save(): TPromise { @@ -282,10 +165,6 @@ export class SettingsEditorModel extends AbstractSettingsModel implements ISetti return this.textFileService.save(this.uri); } - protected findValueMatches(filter: string, setting: ISetting): IRange[] { - return this.settingsModel.findMatches(filter, setting.valueRange, false, false, null, false).map(match => match.range); - } - private parse() { const model = this.settingsModel; const settings: ISetting[] = []; @@ -594,6 +473,8 @@ export class WorkspaceConfigModel extends SettingsEditorModel implements ISettin export class DefaultSettingsEditorModel extends AbstractSettingsModel implements ISettingsEditorModel { + public static MOST_RELEVANT_SECTION_LENGTH = 100; + private _allSettingsGroups: ISettingsGroup[]; private _content: string; private _contentByLines: string[]; @@ -624,8 +505,12 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements return this.settingsGroups[0]; } - public filterSettings(filter: string): IFilterResult { - return this.doFilterSettings(filter, this.settingsGroups); + public filterSettings(filter: string, groupFilter: IGroupFilter, settingFilter: ISettingFilter): IFilterResult { + return this.doFilterSettings(filter, groupFilter, settingFilter); + } + + public findValueMatches(filter: string, setting: ISetting): IRange[] { + return []; } public getPreference(key: string): ISetting { @@ -775,6 +660,10 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements private toContent(mostCommonlyUsed: ISettingsGroup, settingsGroups: ISettingsGroup[]): string { this._contentByLines = []; this._contentByLines.push('['); + this._contentByLines.push('{'); + this._contentByLines.push(...arrays.fill(DefaultSettingsEditorModel.MOST_RELEVANT_SECTION_LENGTH - 3, () => '')); + this._contentByLines.push('}'); + this._contentByLines.push(','); this.pushGroups([mostCommonlyUsed]); this._contentByLines.push(','); this.pushGroups(settingsGroups); @@ -878,12 +767,9 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements } } - protected findValueMatches(filter: string, setting: ISetting): IRange[] { - return []; - } public dispose(): void { - // Not disposable + super.dispose(); } }