diff --git a/src/vs/base/browser/ui/checkbox/checkbox.ts b/src/vs/base/browser/ui/checkbox/checkbox.ts index 10144ee2c719a..86b39b2e234af 100644 --- a/src/vs/base/browser/ui/checkbox/checkbox.ts +++ b/src/vs/base/browser/ui/checkbox/checkbox.ts @@ -73,6 +73,10 @@ export class Checkbox extends Widget { }); } + public get enabled(): boolean { + return this.domNode.getAttribute('aria-disabled') !== 'true'; + } + public focus(): void { this.domNode.focus(); } diff --git a/src/vs/workbench/electron-browser/main.contribution.ts b/src/vs/workbench/electron-browser/main.contribution.ts index 6387586a77711..87de097e47130 100644 --- a/src/vs/workbench/electron-browser/main.contribution.ts +++ b/src/vs/workbench/electron-browser/main.contribution.ts @@ -197,6 +197,16 @@ let workbenchProperties: { [path: string]: IJSONSchema; } = { 'description': nls.localize('openDefaultSettings', "Controls if opening settings also opens an editor showing all default settings."), 'default': true }, + 'workbench.settings.experimentalFuzzySearchEndpoint': { + 'type': 'string', + 'description': nls.localize('experimentalFuzzySearchEndpoint', "Indicates the endpoint to use for the experimental settings search."), + 'default': '' + }, + 'workbench.settings.experimentalFuzzySearchKey': { + 'type': 'string', + 'description': nls.localize('experimentalFuzzySearchKey', "Indicates the key to use for the experimental settings search."), + 'default': '' + }, 'workbench.sideBar.location': { 'type': 'string', 'enum': ['left', 'right'], diff --git a/src/vs/workbench/parts/preferences/browser/media/preferences.css b/src/vs/workbench/parts/preferences/browser/media/preferences.css index 22b490dcd6b04..4084078ea8658 100644 --- a/src/vs/workbench/parts/preferences/browser/media/preferences.css +++ b/src/vs/workbench/parts/preferences/browser/media/preferences.css @@ -73,18 +73,38 @@ padding-right: 32px; } -.settings-header-widget > .settings-count-widget { +.settings-header-widget > .settings-search-controls > .settings-count-widget { margin: 6px 0px; padding: 0px 8px; + border-radius: 2px; + float: left; +} + +.settings-header-widget > .settings-search-controls { position: absolute; right: 10px; - border-radius: 2px; } -.settings-header-widget > .settings-count-widget.hide { +.settings-header-widget > .settings-search-controls > .settings-count-widget.hide { + display: none; +} + +.settings-header-widget > .settings-search-controls > .prefs-fuzzy-search-toggle { + margin: 5px 3px 5px 0px; +} + +.settings-header-widget > .settings-search-controls > .prefs-fuzzy-search-toggle.hidden { display: none; } +.vs .settings-header-widget > .settings-search-controls > .prefs-fuzzy-search-toggle { + background: url('regex.svg') center center no-repeat; +} + +.vs-dark .settings-header-widget > .settings-search-controls > .prefs-fuzzy-search-toggle { + background: url('regex-dark.svg') center center no-repeat; +} + .settings-header-widget > .settings-search-container { flex: 1; } @@ -106,8 +126,13 @@ padding-left:10px; } +.monaco-editor .view-zones > .settings-header-widget { + z-index: 1; +} + .monaco-editor .settings-header-widget .title-container { display: flex; + user-select: none; } .vs .monaco-editor .settings-header-widget .title-container { @@ -130,6 +155,16 @@ white-space: nowrap; } +.monaco-editor .settings-header-widget .title-container .settings-header-fuzzy-link { + margin-left: 4px; + text-decoration: underline; + cursor: pointer; +} + +.monaco-editor .settings-header-widget .title-container .settings-header-fuzzy-link.hidden { + display: none; +} + .monaco-editor .settings-group-title-widget { z-index: 1; } diff --git a/src/vs/workbench/parts/preferences/browser/media/regex-dark.svg b/src/vs/workbench/parts/preferences/browser/media/regex-dark.svg new file mode 100644 index 0000000000000..c303032e6a9af --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/media/regex-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/browser/media/regex.svg b/src/vs/workbench/parts/preferences/browser/media/regex.svg new file mode 100644 index 0000000000000..c677843beef1d --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/media/regex.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index a20434dd807d7..4ae357dac8471 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts @@ -6,8 +6,9 @@ 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 * as DOM from 'vs/base/browser/dom'; -import { Delayer } from 'vs/base/common/async'; +import { Delayer, ThrottledDelayer } from 'vs/base/common/async'; import { Dimension, Builder } from 'vs/base/browser/builder'; import { ArrayNavigator, INavigator } from 'vs/base/common/iterator'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -29,6 +30,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, PreferencesSearchModel } 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'; @@ -85,10 +87,10 @@ export class DefaultPreferencesEditorInput extends ResourceEditorInput { } matches(other: any): boolean { - if (!super.matches(other)) { - return false; + if (other instanceof DefaultPreferencesEditorInput) { + return true; } - if (!(other instanceof DefaultPreferencesEditorInput)) { + if (!super.matches(other)) { return false; } return true; @@ -106,8 +108,10 @@ export class PreferencesEditor extends BaseEditor { private settingsTargetsWidget: SettingsTargetsWidget; private sideBySidePreferencesWidget: SideBySidePreferencesWidget; private preferencesRenderers: PreferencesRenderers; + private searchProvider: PreferencesSearchProvider; private delayedFilterLogging: Delayer; + private filterThrottle: ThrottledDelayer; private latestEmptyFilters: string[] = []; private lastFocusedWidget: SearchWidget | SideBySidePreferencesWidget = null; @@ -126,6 +130,8 @@ 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.searchProvider = this.instantiationService.createInstance(PreferencesSearchProvider); + this.filterThrottle = new ThrottledDelayer(200); } public createEditor(parent: Builder): void { @@ -139,7 +145,9 @@ 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.searchWidget.setFuzzyToggleVisible(this.searchProvider.remoteSearchEnabled); + this._register(this.searchProvider.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; @@ -153,6 +161,11 @@ export class PreferencesEditor extends BaseEditor { this.preferencesRenderers = this._register(new PreferencesRenderers()); this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(() => this.onWorkspaceFoldersChanged())); this._register(this.workspaceContextService.onDidChangeWorkbenchState(() => this.onWorkbenchStateChanged())); + + this._register(this.preferencesRenderers.onTriggeredFuzzy(() => { + this.searchWidget.fuzzyEnabled = true; + this.filterPreferences(); + })); } public clearSearchResults(): void { @@ -233,10 +246,22 @@ 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.onInputChanged(); }); } + private onInputChanged(): void { + if (this.searchWidget.fuzzyEnabled) { + this.triggerThrottledFilter(); + } else { + this.filterPreferences(); + } + } + + private triggerThrottledFilter(): void { + this.filterThrottle.trigger(() => this.filterPreferences()); + } + private getSettingsConfigurationTarget(resource: URI): ConfigurationTarget { if (this.preferencesService.userSettingsResource.toString() === resource.toString()) { return ConfigurationTarget.USER; @@ -300,14 +325,16 @@ export class PreferencesEditor extends BaseEditor { promise.done(value => this.preferencesService.switchSettings(this.getSettingsConfigurationTarget(resource), resource)); } - 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)); + private filterPreferences(): TPromise { + const filter = this.searchWidget.getValue().trim(); + return this.preferencesRenderers.filterPreferences(filter, this.searchProvider, this.searchWidget.fuzzyEnabled).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)); + }, onUnexpectedError); } private showSearchResultsMessage(count: number): string { @@ -381,9 +408,13 @@ class PreferencesRenderers extends Disposable { private _defaultPreferencesRenderer: IPreferencesRenderer; private _editablePreferencesRenderer: IPreferencesRenderer; private _settingsNavigator: SettingsNavigator; + private _filtersInProgress: TPromise[]; private _disposables: IDisposable[] = []; + private _onTriggeredFuzzy: Emitter = new Emitter(); + public onTriggeredFuzzy: Event = this._onTriggeredFuzzy.event; + public get defaultPreferencesRenderer(): IPreferencesRenderer { return this._defaultPreferencesRenderer; } @@ -398,6 +429,9 @@ class PreferencesRenderers extends Disposable { this._defaultPreferencesRenderer.onUpdatePreference(({ key, value, source }) => this._updatePreference(key, value, source, this._editablePreferencesRenderer), this, this._disposables); this._defaultPreferencesRenderer.onFocusPreference(preference => this._focusPreference(preference, this._editablePreferencesRenderer), this, this._disposables); this._defaultPreferencesRenderer.onClearFocusPreference(preference => this._clearFocus(preference, this._editablePreferencesRenderer), this, this._disposables); + if (this._defaultPreferencesRenderer.onTriggeredFuzzy) { + this._register(this._defaultPreferencesRenderer.onTriggeredFuzzy(() => this._onTriggeredFuzzy.fire())); + } } } } @@ -406,19 +440,37 @@ 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, searchProvider: PreferencesSearchProvider, fuzzy: boolean): TPromise { + if (this._filtersInProgress) { + // Resolved/rejected promises have no .cancel() + this._filtersInProgress.forEach(p => p.cancel && p.cancel()); + } + + const searchModel = searchProvider.startSearch(filter, fuzzy); + this._filtersInProgress = [ + this._filterPreferences(searchModel, searchProvider, this._defaultPreferencesRenderer), + this._filterPreferences(searchModel, 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 : []); + 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); @@ -428,13 +480,17 @@ class PreferencesRenderers extends Disposable { return preferencesRenderer ? (preferencesRenderer.preferencesModel).settingsGroups : []; } - private _filterPreferences(filter: string, preferencesRenderer: IPreferencesRenderer): IFilterResult { - let filterResult = null; + private _filterPreferences(searchModel: PreferencesSearchModel, searchProvider: PreferencesSearchProvider, preferencesRenderer: IPreferencesRenderer): TPromise { if (preferencesRenderer) { - filterResult = filter ? (preferencesRenderer.preferencesModel).filterSettings(filter) : null; - preferencesRenderer.filterPreferences(filterResult); + const prefSearchP = searchModel.filterPreferences(preferencesRenderer.preferencesModel); + + return prefSearchP.then(filterResult => { + preferencesRenderer.filterPreferences(filterResult, searchProvider.remoteSearchEnabled); + return filterResult; + }); } - return filterResult; + + return TPromise.wrap(null); } private _focusPreference(preference: ISetting, preferencesRenderer: IPreferencesRenderer): void { @@ -550,6 +606,9 @@ class SideBySidePreferencesWidget extends Widget { } public clearInput(): void { + if (this.defaultPreferencesEditor) { + this.defaultPreferencesEditor.clearInput(); + } if (this.editablePreferencesEditor) { this.editablePreferencesEditor.clearInput(); } @@ -765,6 +824,14 @@ export class DefaultPreferencesEditor extends BaseTextEditor { .then(editorModel => this.getControl().setModel((editorModel).textEditorModel))); } + public clearInput(): void { + // Clear Model + this.getControl().setModel(null); + + // Pass to super + super.clearInput(); + } + public layout(dimension: Dimension) { this.getControl().layout(dimension); } @@ -854,6 +921,7 @@ abstract class AbstractSettingsEditorContribution extends Disposable { if (preferencesRenderer.associatedPreferencesModel) { preferencesRenderer.associatedPreferencesModel.dispose(); } + preferencesRenderer.preferencesModel.dispose(); preferencesRenderer.dispose(); } }); @@ -910,16 +978,16 @@ class SettingsEditorContribution extends AbstractSettingsEditorContribution impl protected _createPreferencesRenderer(): TPromise> { if (this.isSettingsModel()) { - return TPromise.join([this.preferencesService.createPreferencesEditorModel(this.preferencesService.defaultSettingsResource), this.preferencesService.createPreferencesEditorModel(this.editor.getModel().uri)]) - .then(([defaultSettingsModel, settingsModel]) => { + return this.preferencesService.createPreferencesEditorModel(this.editor.getModel().uri) + .then(settingsModel => { if (settingsModel instanceof SettingsEditorModel && this.editor.getModel()) { switch (settingsModel.configurationTarget) { case ConfigurationTarget.USER: - return this.instantiationService.createInstance(UserSettingsRenderer, this.editor, settingsModel, defaultSettingsModel); + return this.instantiationService.createInstance(UserSettingsRenderer, this.editor, settingsModel); case ConfigurationTarget.WORKSPACE: - return this.instantiationService.createInstance(WorkspaceSettingsRenderer, this.editor, settingsModel, defaultSettingsModel); + return this.instantiationService.createInstance(WorkspaceSettingsRenderer, this.editor, settingsModel); case ConfigurationTarget.WORKSPACE_FOLDER: - return this.instantiationService.createInstance(FolderSettingsRenderer, this.editor, settingsModel, defaultSettingsModel); + return this.instantiationService.createInstance(FolderSettingsRenderer, this.editor, settingsModel); } } return null; diff --git a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts index 08edba32488c9..4795b7be61450 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 { 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'; @@ -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, DefaultSettingsHeaderWidget, 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 { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; @@ -40,21 +41,22 @@ export interface IPreferencesRenderer extends IDisposable { onFocusPreference: Event; onClearFocusPreference: Event; onUpdatePreference: Event<{ key: string, value: any, source: T }>; + onTriggeredFuzzy?: Event; render(): void; updatePreference(key: string, value: any, source: T): void; - filterPreferences(filterResult: IFilterResult): void; + filterPreferences(filterResult: IFilterResult, fuzzySearchAvailable: boolean): void; focusPreference(setting: T): void; clearFocus(setting: T): void; } - export class UserSettingsRenderer extends Disposable implements IPreferencesRenderer { private settingHighlighter: SettingHighlighter; private editSettingActionRenderer: EditSettingRenderer; private highlightMatchesRenderer: HighlightMatchesRenderer; private modelChangeDelayer: Delayer = new Delayer(200); + private _associatedPreferencesModel: IPreferencesEditorModel; private _onFocusPreference: Emitter = new Emitter(); public readonly onFocusPreference: Event = this._onFocusPreference.event; @@ -67,7 +69,7 @@ export class UserSettingsRenderer extends Disposable implements IPreferencesRend private filterResult: IFilterResult; - constructor(protected editor: ICodeEditor, public readonly preferencesModel: SettingsEditorModel, private _associatedPreferencesModel: IPreferencesEditorModel, + constructor(protected editor: ICodeEditor, public readonly preferencesModel: SettingsEditorModel, @IPreferencesService protected preferencesService: IPreferencesService, @ITelemetryService private telemetryService: ITelemetryService, @ITextFileService private textFileService: ITextFileService, @@ -76,7 +78,6 @@ export class UserSettingsRenderer extends Disposable implements IPreferencesRend @IInstantiationService protected instantiationService: IInstantiationService ) { super(); - this._register(preferencesModel); this.settingHighlighter = this._register(instantiationService.createInstance(SettingHighlighter, editor, this._onFocusPreference, this._onClearFocusPreference)); this.highlightMatchesRenderer = this._register(instantiationService.createInstance(HighlightMatchesRenderer, editor)); this.editSettingActionRenderer = this._register(this.instantiationService.createInstance(EditSettingRenderer, this.editor, this.preferencesModel, this.settingHighlighter)); @@ -176,7 +177,7 @@ export class WorkspaceSettingsRenderer extends UserSettingsRenderer implements I private untrustedSettingRenderer: UnsupportedWorkspaceSettingsRenderer; private workspaceConfigurationRenderer: WorkspaceConfigurationRenderer; - constructor(editor: ICodeEditor, preferencesModel: SettingsEditorModel, associatedPreferencesModel: IPreferencesEditorModel, + constructor(editor: ICodeEditor, preferencesModel: SettingsEditorModel, @IPreferencesService preferencesService: IPreferencesService, @ITelemetryService telemetryService: ITelemetryService, @ITextFileService textFileService: ITextFileService, @@ -184,7 +185,7 @@ export class WorkspaceSettingsRenderer extends UserSettingsRenderer implements I @IMessageService messageService: IMessageService, @IInstantiationService instantiationService: IInstantiationService ) { - super(editor, preferencesModel, associatedPreferencesModel, preferencesService, telemetryService, textFileService, configurationService, messageService, instantiationService); + super(editor, preferencesModel, preferencesService, telemetryService, textFileService, configurationService, messageService, instantiationService); this.untrustedSettingRenderer = this._register(instantiationService.createInstance(UnsupportedWorkspaceSettingsRenderer, editor, preferencesModel)); this.workspaceConfigurationRenderer = this._register(instantiationService.createInstance(WorkspaceConfigurationRenderer, editor, preferencesModel)); } @@ -204,7 +205,7 @@ export class FolderSettingsRenderer extends UserSettingsRenderer implements IPre private unsupportedWorkbenchSettingsRenderer: UnsupportedWorkbenchSettingsRenderer; - constructor(editor: ICodeEditor, preferencesModel: SettingsEditorModel, associatedPreferencesModel: IPreferencesEditorModel, + constructor(editor: ICodeEditor, preferencesModel: SettingsEditorModel, @IPreferencesService preferencesService: IPreferencesService, @ITelemetryService telemetryService: ITelemetryService, @ITextFileService textFileService: ITextFileService, @@ -212,7 +213,7 @@ export class FolderSettingsRenderer extends UserSettingsRenderer implements IPre @IMessageService messageService: IMessageService, @IInstantiationService instantiationService: IInstantiationService ) { - super(editor, preferencesModel, associatedPreferencesModel, preferencesService, telemetryService, textFileService, configurationService, messageService, instantiationService); + super(editor, preferencesModel, preferencesService, telemetryService, textFileService, configurationService, messageService, instantiationService); this.unsupportedWorkbenchSettingsRenderer = this._register(instantiationService.createInstance(UnsupportedWorkbenchSettingsRenderer, editor, preferencesModel)); } @@ -235,6 +236,7 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR private filteredMatchesRenderer: FilteredMatchesRenderer; private hiddenAreasRenderer: HiddenAreasRenderer; private editSettingActionRenderer: EditSettingRenderer; + 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; @@ -245,6 +247,8 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR private _onClearFocusPreference: Emitter = new Emitter(); public readonly onClearFocusPreference: Event = this._onClearFocusPreference.event; + public readonly onTriggeredFuzzy: Event; + private filterResult: IFilterResult; constructor(protected editor: ICodeEditor, public readonly preferencesModel: DefaultSettingsEditorModel, @@ -258,11 +262,17 @@ 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.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.hiddenAreasRenderer = this._register(instantiationService.createInstance(HiddenAreasRenderer, editor, hiddenAreasProviders)); this._register(this.settingsGroupTitleRenderer.onHiddenAreasChanged(() => this.hiddenAreasRenderer.render())); + + this.onTriggeredFuzzy = this.settingsHeaderRenderer.onClick; } public get associatedPreferencesModel(): IPreferencesEditorModel { @@ -277,28 +287,32 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR public render() { this.settingsGroupTitleRenderer.render(this.preferencesModel.settingsGroups); this.editSettingActionRenderer.render(this.preferencesModel.settingsGroups, this._associatedPreferencesModel); + this.feedbackWidgetRenderer.render(null); this.hiddenAreasRenderer.render(); this.settingHighlighter.clear(true); - this.settingsGroupTitleRenderer.showGroup(1); + this.settingsGroupTitleRenderer.showGroup(0); this.hiddenAreasRenderer.render(); } - public filterPreferences(filterResult: IFilterResult): void { + public filterPreferences(filterResult: IFilterResult, fuzzySearchAvailable: boolean): void { this.filterResult = filterResult; - if (!filterResult) { - this.settingHighlighter.clear(true); - this.filteredMatchesRenderer.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); + if (filterResult) { + this.filteredMatchesRenderer.render(filterResult, this.preferencesModel.settingsGroups); this.settingsGroupTitleRenderer.render(filterResult.filteredGroups); + this.feedbackWidgetRenderer.render(filterResult); + this.settingsHeaderRenderer.render(filterResult, fuzzySearchAvailable); this.settingHighlighter.clear(true); this.editSettingActionRenderer.render(filterResult.filteredGroups, this._associatedPreferencesModel); + } else { + this.settingHighlighter.clear(true); + this.filteredMatchesRenderer.render(null, this.preferencesModel.settingsGroups); + this.feedbackWidgetRenderer.render(null); + this.settingsHeaderRenderer.render(null); + this.settingsGroupTitleRenderer.render(this.preferencesModel.settingsGroups); + this.settingsGroupTitleRenderer.showGroup(0); + this.editSettingActionRenderer.render(this.preferencesModel.settingsGroups, this._associatedPreferencesModel); } + this.hiddenAreasRenderer.render(); } @@ -365,6 +379,9 @@ 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 + const lastGroup = tail(this.settingsGroups); return [ { startLineNumber: 1, @@ -378,6 +395,12 @@ export class StaticContentHidingRenderer extends Disposable implements HiddenAre endLineNumber: this.settingsGroups[0].range.endLineNumber + 4, endColumn: model.getLineMaxColumn(this.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), @@ -391,20 +414,20 @@ export class StaticContentHidingRenderer extends Disposable implements HiddenAre class DefaultSettingsHeaderRenderer extends Disposable { - private settingsHeaderWidget: SettingsHeaderWidget; + private settingsHeaderWidget: DefaultSettingsHeaderWidget; + public onClick: Event; constructor(private editor: ICodeEditor, scope: ConfigurationScope) { super(); const title = scope === ConfigurationScope.RESOURCE ? nls.localize('defaultFolderSettingsTitle', "Default Folder Settings") : nls.localize('defaultSettingsTitle', "Default Settings"); - this.settingsHeaderWidget = this._register(new SettingsHeaderWidget(editor, title)); + this.settingsHeaderWidget = this._register(new DefaultSettingsHeaderWidget(editor, title)); + this.onClick = this.settingsHeaderWidget.onClick; } - public render(settingsGroups: ISettingsGroup[]) { - if (settingsGroups.length) { - this.settingsHeaderWidget.setMessage(nls.localize('defaultSettings', "Place your settings in the right hand side editor to override.")); - } else { - this.settingsHeaderWidget.setMessage(nls.localize('noSettingsFound', "No Settings Found.")); - } + public render(filterResult: IFilterResult, fuzzySearchAvailable = false) { + const hasSettings = !filterResult || filterResult.filteredGroups.length > 0; + const promptFuzzy = fuzzySearchAvailable && filterResult && !filterResult.metadata; + this.settingsHeaderWidget.toggleMessage(hasSettings, promptFuzzy); } } @@ -434,9 +457,17 @@ 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()) { + if (group.sections.every(sect => sect.settings.length === 0)) { + continue; + } + const settingsGroupTitleWidget = this.instantiationService.createInstance(SettingsGroupTitleWidget, this.editor, group); settingsGroupTitleWidget.render(); this.settingsGroupTitleWidgets.push(settingsGroupTitleWidget); @@ -446,9 +477,11 @@ export class SettingsGroupTitleRenderer extends Disposable implements HiddenArea this.settingsGroupTitleWidgets.reverse(); } - public showGroup(group: number) { - this.hiddenGroups = this.settingsGroups.filter((g, i) => i !== group - 1); - for (const groupTitleWidget of this.settingsGroupTitleWidgets.filter((g, i) => i !== group - 1)) { + public showGroup(groupIdx: number) { + const shownGroup = this.settingsGroupTitleWidgets[groupIdx].settingsGroup; + + this.hiddenGroups = this.settingsGroups.filter(g => g !== shownGroup); + for (const groupTitleWidget of this.settingsGroupTitleWidgets.filter(widget => widget.settingsGroup !== shownGroup)) { groupTitleWidget.toggleCollapse(true); } this._onHiddenAreasChanged.fire(); @@ -519,6 +552,115 @@ export class HiddenAreasRenderer extends Disposable { } } +export class FeedbackWidgetRenderer extends Disposable { + private static COMMENT_TEXT = 'Modify the below results to match your expectations. Assign scores to indicate their relevance. Replace this comment with any text feedback.'; + + private _feedbackWidget: FloatingClickWidget; + private _currentResult: IFilterResult; + + constructor(private editor: ICodeEditor, + @IInstantiationService private instantiationService: IInstantiationService, + @IWorkbenchEditorService private editorService: IWorkbenchEditorService, + @ITelemetryService private telemetryService: ITelemetryService + ) { + super(); + } + + public render(result: IFilterResult): void { + this._currentResult = result; + if (result && result.metadata) { + 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 { + const result = this._currentResult; + const actualResults = result.filteredGroups[0] ? result.filteredGroups[0].sections[0].settings.map(setting => setting.key) : []; + + const feedbackQuery = {}; + feedbackQuery['_comment'] = FeedbackWidgetRenderer.COMMENT_TEXT; + feedbackQuery['queryString'] = result.query; + feedbackQuery['resultScores'] = {}; + actualResults.forEach(settingKey => { + feedbackQuery['resultScores'][settingKey] = 10; + }); + + const contents = JSON.stringify(feedbackQuery, undefined, ' '); + this.editorService.openEditor({ contents }, /*sideBySide=*/true).then(feedbackEditor => { + const sendFeedbackWidget = this._register(this.instantiationService.createInstance(FloatingClickWidget, feedbackEditor.getControl(), 'Send feedback', null)); + sendFeedbackWidget.render(); + + this._register(sendFeedbackWidget.onClick(() => { + if (this.sendFeedback(feedbackEditor.getControl() as ICodeEditor, result, actualResults)) { + sendFeedbackWidget.dispose(); + } + })); + }); + } + + private sendFeedback(feedbackEditor: ICodeEditor, result: IFilterResult, actualResults: string[]): boolean { + const model = feedbackEditor.getModel(); + const expectedQueryLines = model.getLinesContent(); + let expectedQuery: string; + try { + expectedQuery = JSON.parse(expectedQueryLines.join('\n')); + if (expectedQuery['_comment'] === FeedbackWidgetRenderer.COMMENT_TEXT) { + delete expectedQuery['_comment']; + } + } catch (e) { + // invalid JSON + } + + if (expectedQuery) { + /* __GDPR__ + "settingsSearchResultFeedback" : { + "query" : { "classification": "CustomContent", "purpose": "FeatureInsight" }, + "userComment" : { "classification": "CustomerContent", "purpose": "FeatureInsight" }, + "actualResults" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "expectedResults" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "url" : { "classification": "CustomerContent", "purpose": "FeatureInsight" }, + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "timestamp" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('settingsSearchResultFeedback', { + query: result.query, + actualResults, + expectedQuery, + url: result.metadata.remoteUrl, + duration: result.metadata.duration, + timestamp: result.metadata.timestamp + }); + return true; + } + + return false; + } + + 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[] = []; @@ -530,7 +672,7 @@ export class FilteredMatchesRenderer extends Disposable implements HiddenAreasPr super(); } - public render(result: IFilterResult): void { + public render(result: IFilterResult, allSettingsGroups: ISettingsGroup[]): void { const model = this.editor.getModel(); this.hiddenAreas = []; this.editor.changeDecorations(changeAccessor => { @@ -541,6 +683,8 @@ export class FilteredMatchesRenderer extends Disposable implements HiddenAreasPr this.editor.changeDecorations(changeAccessor => { this.decorationIds = changeAccessor.deltaDecorations(this.decorationIds, result.matches.map(match => this.createDecoration(match, model))); }); + } else { + this.hiddenAreas = this.computeHiddenRanges(allSettingsGroups, allSettingsGroups, model); } } @@ -559,7 +703,7 @@ export class FilteredMatchesRenderer extends Disposable implements HiddenAreasPr const notMatchesRanges: IRange[] = []; for (const group of allSettingsGroups) { const filteredGroup = filteredGroups.filter(g => g.title === group.title)[0]; - if (!filteredGroup) { + if (!filteredGroup || filteredGroup.sections.every(sect => sect.settings.length === 0)) { notMatchesRanges.push({ startLineNumber: group.range.startLineNumber - 1, startColumn: model.getLineMinColumn(group.range.startLineNumber - 1), @@ -886,8 +1030,8 @@ class EditSettingRenderer extends Disposable { } private getDefaultActions(setting: ISetting): IAction[] { - const settingInOtherModel = this.associatedPreferencesModel.getPreference(setting.key); if (this.isDefaultSettings()) { + const settingInOtherModel = this.associatedPreferencesModel.getPreference(setting.key); return [{ id: 'setDefaultValue', label: settingInOtherModel ? nls.localize('replaceDefaultValue', "Replace in Settings") : nls.localize('copyDefaultValue', "Copy to Settings"), 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 0000000000000..10be6e89bdd8c --- /dev/null +++ b/src/vs/workbench/parts/preferences/browser/preferencesSearch.ts @@ -0,0 +1,326 @@ +/*--------------------------------------------------------------------------------------------- + * 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 Event, { Emitter } from 'vs/base/common/event'; +import { ISettingsEditorModel, IFilterResult, ISetting, ISettingsGroup, IWorkbenchSettingsConfiguration, IFilterMetadata } 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'; +import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; + +export interface IEndpointDetails { + urlBase: string; + key: string; +} + +export class PreferencesSearchProvider { + private _onRemoteSearchEnablementChanged = new Emitter(); + public onRemoteSearchEnablementChanged: Event = this._onRemoteSearchEnablementChanged.event; + + constructor( @IWorkspaceConfigurationService private configurationService: IWorkspaceConfigurationService) { + configurationService.onDidChangeConfiguration(() => this._onRemoteSearchEnablementChanged.fire(this.remoteSearchEnabled)); + } + + get remoteSearchEnabled(): boolean { + const endpoint = this.endpoint; + return !!endpoint.urlBase && !!endpoint.key; + } + + get endpoint(): IEndpointDetails { + const workbenchSettings = this.configurationService.getConfiguration().workbench.settings; + return { + urlBase: workbenchSettings.experimentalFuzzySearchEndpoint, + key: workbenchSettings.experimentalFuzzySearchKey + }; + } + + startSearch(filter: string, remote: boolean): PreferencesSearchModel { + return new PreferencesSearchModel(this, filter, remote); + } +} + +export class PreferencesSearchModel { + private _localProvider: LocalSearchProvider; + private _remoteProvider: RemoteSearchProvider; + + constructor(private provider: PreferencesSearchProvider, private filter: string, remote: boolean) { + this._localProvider = new LocalSearchProvider(filter); + + if (remote && filter) { + this._remoteProvider = new RemoteSearchProvider(filter, this.provider.endpoint); + } + } + + filterPreferences(preferencesModel: ISettingsEditorModel): TPromise { + if (!this.filter) { + return TPromise.wrap(null); + } + + if (this._remoteProvider) { + return this._remoteProvider.filterPreferences(preferencesModel).then(null, err => { + return this._localProvider.filterPreferences(preferencesModel); + }); + } else { + 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)); + } +} + +export interface IRemoteScores { + [key: string]: number; +} + +interface IRemoteResult { + metadata: IFilterMetadata; + scores: IRemoteScores; +} + +class RemoteSearchProvider { + private _filter: string; + private _remoteSearchP: TPromise; + + constructor(filter: string, endpoint: IEndpointDetails) { + this._filter = filter; + this._remoteSearchP = filter ? getSettingsFromBing(filter, endpoint) : TPromise.wrap(null); + } + + filterPreferences(preferencesModel: ISettingsEditorModel): TPromise { + return this._remoteSearchP.then(remoteResult => { + const settingFilter = (setting: ISetting) => { + if (!!remoteResult.scores[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; + } + }; + + if (remoteResult) { + const sortedNames = Object.keys(remoteResult.scores).sort((a, b) => remoteResult.scores[b] - remoteResult.scores[a]); + const result = preferencesModel.filterSettings(this._filter, group => null, settingFilter, sortedNames); + result.metadata = remoteResult.metadata; + return result; + } else { + return null; + } + }); + } +} + +function getSettingsFromBing(filter: string, endpoint: IEndpointDetails): TPromise { + const url = prepareUrl(filter, endpoint); + console.log('fetching: ' + url); + const start = Date.now(); + const p = fetch(url, { + headers: new Headers({ + 'User-Agent': 'request', + 'Content-Type': 'application/json; charset=utf-8', + 'api-key': endpoint.key + }) + }) + .then(r => r.json()) + .then(result => { + const timestamp = Date.now(); + const duration = timestamp - start; + console.log('time: ' + duration / 1000); + const suggestions = (result.value || []) + .map(r => ({ + name: r.setting || r.Setting, + score: r['@search.score'] + })); + + const scores = Object.create(null); + suggestions.forEach(s => { + const name = s.name + .replace(/^"/, '') + .replace(/"$/, ''); + scores[name] = s.score; + }); + + return { + metadata: { + remoteUrl: url, + duration, + timestamp + }, + scores + }; + }); + + return TPromise.as(p as any); +} + +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, endpoint: IEndpointDetails): 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}`; +} + +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 + }; + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/browser/preferencesService.ts b/src/vs/workbench/parts/preferences/browser/preferencesService.ts index fdf08f805c27e..5d866bac7ae8a 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesService.ts @@ -8,7 +8,6 @@ import * as network from 'vs/base/common/network'; import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; import URI from 'vs/base/common/uri'; -import { ResourceMap } from 'vs/base/common/map'; import * as labels from 'vs/base/common/labels'; import * as strings from 'vs/base/common/strings'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -18,7 +17,7 @@ import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/edi import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { Position as EditorPosition, IEditor, IEditorOptions } from 'vs/platform/editor/common/editor'; -import { ICommonCodeEditor } from 'vs/editor/common/editorCommon'; +import { ICommonCodeEditor, IModel } from 'vs/editor/common/editorCommon'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; @@ -27,7 +26,7 @@ import { IExtensionService } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IPreferencesService, IPreferencesEditorModel, ISetting, getSettingsTargetName, FOLDER_SETTINGS_PATH, DEFAULT_SETTINGS_EDITOR_SETTING } from 'vs/workbench/parts/preferences/common/preferences'; -import { SettingsEditorModel, DefaultSettingsEditorModel, DefaultKeybindingsEditorModel, defaultKeybindingsContents, WorkspaceConfigModel } from 'vs/workbench/parts/preferences/common/preferencesModels'; +import { SettingsEditorModel, DefaultSettingsEditorModel, DefaultKeybindingsEditorModel, defaultKeybindingsContents, WorkspaceConfigModel, DefaultSettingsModel } from 'vs/workbench/parts/preferences/common/preferencesModels'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DefaultPreferencesEditorInput, PreferencesEditorInput } from 'vs/workbench/parts/preferences/browser/preferencesEditor'; import { KeybindingsEditorInput } from 'vs/workbench/parts/preferences/browser/keybindingsEditor'; @@ -40,6 +39,7 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { IModeService } from 'vs/editor/common/services/modeService'; const emptyEditableSettingsContent = '{\n}'; @@ -47,12 +47,15 @@ export class PreferencesService extends Disposable implements IPreferencesServic _serviceBrand: any; - // TODO:@sandy merge these models into editor inputs by extending resource editor model - private defaultPreferencesEditorModels: ResourceMap>>; private lastOpenedSettingsInput: PreferencesEditorInput = null; private _onDispose: Emitter = new Emitter(); + private _defaultSettingsUriCounter = 0; + private _defaultSettingsContentModel: DefaultSettingsModel; + private _defaultResourceSettingsUriCounter = 0; + private _defaultResourceSettingsContentModel: DefaultSettingsModel; + constructor( @IWorkbenchEditorService private editorService: IWorkbenchEditorService, @IEditorGroupService private editorGroupService: IEditorGroupService, @@ -69,10 +72,10 @@ export class PreferencesService extends Disposable implements IPreferencesServic @IExtensionService private extensionService: IExtensionService, @IKeybindingService keybindingService: IKeybindingService, @IModelService private modelService: IModelService, - @IJSONEditingService private jsonEditingService: IJSONEditingService + @IJSONEditingService private jsonEditingService: IJSONEditingService, + @IModeService private modeService: IModeService ) { super(); - this.defaultPreferencesEditorModels = new ResourceMap>>(); this.editorGroupService.onEditorsChanged(() => { const activeEditorInput = this.editorService.getActiveEditorInput(); if (activeEditorInput instanceof PreferencesEditorInput) { @@ -92,8 +95,6 @@ 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 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' }); @@ -109,54 +110,38 @@ export class PreferencesService extends Disposable implements IPreferencesServic return this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE_FOLDER, resource); } - resolveContent(uri: URI): TPromise { - const workspaceSettingsUri = this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE); - if (workspaceSettingsUri && workspaceSettingsUri.toString() === uri.toString()) { - return this.resolveSettingsContentFromWorkspaceConfiguration(); + resolveModel(uri: URI): TPromise { + if (this.isDefaultSettingsResource(uri) || this.isDefaultResourceSettingsResource(uri)) { + return this.extensionService.onReady() + .then(() => { + const scope = this.isDefaultSettingsResource(uri) ? ConfigurationScope.WINDOW : ConfigurationScope.RESOURCE; + const settingsModel = this.getDefaultSettingsModel(scope); + const mode = this.modeService.getOrCreateMode('json'); + const model = this._register(this.modelService.createModel(settingsModel.content, mode, uri)); + return model; + }); } - return this.createPreferencesEditorModel(uri) - .then(preferencesEditorModel => preferencesEditorModel ? preferencesEditorModel.content : null); - } - createPreferencesEditorModel(uri: URI): TPromise> { - let promise = this.defaultPreferencesEditorModels.get(uri); - if (promise) { - return promise; + if (this.defaultKeybindingsResource.toString() === uri.toString()) { + return this.extensionService.onReady() + .then(() => this.instantiationService.createInstance(DefaultKeybindingsEditorModel, uri).model); } - if (this.defaultSettingsResource.toString() === uri.toString()) { - promise = TPromise.join([this.extensionService.onReady(), this.fetchMostCommonlyUsedSettings()]) - .then(result => { - const mostCommonSettings = result[1]; - const model = this.instantiationService.createInstance(DefaultSettingsEditorModel, uri, mostCommonSettings, ConfigurationScope.WINDOW); - return model; - }); - this.defaultPreferencesEditorModels.set(uri, promise); - return promise; - } + return TPromise.as(null); + } - if (this.defaultResourceSettingsResource.toString() === uri.toString()) { - promise = TPromise.join([this.extensionService.onReady(), this.fetchMostCommonlyUsedSettings()]) - .then(result => { - const mostCommonSettings = result[1]; - const model = this.instantiationService.createInstance(DefaultSettingsEditorModel, uri, mostCommonSettings, ConfigurationScope.RESOURCE); - return model; - }); - this.defaultPreferencesEditorModels.set(uri, promise); - return promise; + createPreferencesEditorModel(uri: URI): TPromise> { + if (this.isDefaultSettingsResource(uri) || this.isDefaultResourceSettingsResource(uri)) { + return this.createDefaultSettingsEditorModel(uri); } if (this.defaultKeybindingsResource.toString() === uri.toString()) { const model = this.instantiationService.createInstance(DefaultKeybindingsEditorModel, uri); - promise = TPromise.wrap(model); - this.defaultPreferencesEditorModels.set(uri, promise); - return promise; + return TPromise.wrap(model); } if (this.workspaceConfigSettingsResource.toString() === uri.toString()) { - promise = this.createEditableSettingsEditorModel(ConfigurationTarget.WORKSPACE, uri); - this.defaultPreferencesEditorModels.set(uri, promise); - return promise; + return this.createEditableSettingsEditorModel(ConfigurationTarget.WORKSPACE, uri); } if (this.getEditableSettingsURI(ConfigurationTarget.USER).toString() === uri.toString()) { @@ -268,11 +253,20 @@ 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.WORKSPACE_FOLDER) { - return this.defaultResourceSettingsResource; + return URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: `/${this._defaultResourceSettingsUriCounter++}/resourceSettings.json` }); + } else { + return URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: `/${this._defaultSettingsUriCounter++}/settings.json` }); } - return this.defaultSettingsResource; } private getPreferencesEditorInputName(target: ConfigurationTarget, resource: URI): string { @@ -298,17 +292,27 @@ export class PreferencesService extends Disposable implements IPreferencesServic return TPromise.wrap(null); } - private resolveSettingsContentFromWorkspaceConfiguration(): TPromise { - if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { - return this.textModelResolverService.createModelReference(this.contextService.getWorkspace().configuration) - .then(reference => { - const model = reference.object.textEditorModel; - const settingsContent = WorkspaceConfigModel.getSettingsContentFromConfigContent(model.getValue()); - reference.dispose(); - return TPromise.as(settingsContent ? settingsContent : emptyEditableSettingsContent); - }); + private createDefaultSettingsEditorModel(defaultSettingsUri: URI): TPromise { + return this.textModelResolverService.createModelReference(defaultSettingsUri) + .then(reference => { + const scope = this.isDefaultSettingsResource(defaultSettingsUri) ? ConfigurationScope.WINDOW : ConfigurationScope.RESOURCE; + return this.instantiationService.createInstance(DefaultSettingsEditorModel, defaultSettingsUri, reference, scope, this.getDefaultSettingsModel(scope).settingsGroups); + }); + } + + private getDefaultSettingsModel(scope: ConfigurationScope): DefaultSettingsModel { + switch (scope) { + case ConfigurationScope.WINDOW: + if (!this._defaultSettingsContentModel) { + this._defaultSettingsContentModel = new DefaultSettingsModel(this.getMostCommonlyUsedSettings(), scope); + } + return this._defaultSettingsContentModel; + case ConfigurationScope.RESOURCE: + if (!this._defaultResourceSettingsContentModel) { + this._defaultResourceSettingsContentModel = new DefaultSettingsModel(this.getMostCommonlyUsedSettings(), scope); + } + return this._defaultResourceSettingsContentModel; } - return TPromise.as(null); } private getEditableSettingsURI(configurationTarget: ConfigurationTarget, resource?: URI): URI { @@ -349,8 +353,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic }); } - private fetchMostCommonlyUsedSettings(): TPromise { - return TPromise.wrap([ + private getMostCommonlyUsedSettings(): string[] { + return [ 'files.autoSave', 'editor.fontSize', 'editor.fontFamily', @@ -362,7 +366,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic 'editor.wordWrap', 'files.exclude', 'files.associations' - ]); + ]; } private getPosition(language: string, codeEditor: ICommonCodeEditor): TPromise { @@ -407,7 +411,6 @@ export class PreferencesService extends Disposable implements IPreferencesServic public dispose(): void { this._onDispose.fire(); - this.defaultPreferencesEditorModels.clear(); super.dispose(); } } diff --git a/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts index 0eeeadfeb475b..cf03fd27d6f9b 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts @@ -10,6 +10,7 @@ import * as DOM from 'vs/base/browser/dom'; import { TPromise } from 'vs/base/common/winjs.base'; import { Disposable } 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 } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -22,7 +23,7 @@ import { ISettingsGroup, IPreferencesService, getSettingsTargetName } from 'vs/w import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IAction, IActionRunner } from 'vs/base/common/actions'; -import { attachInputBoxStyler, attachStylerCallback, attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; +import { attachInputBoxStyler, attachStylerCallback, attachSelectBoxStyler, attachCheckboxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Position } from 'vs/editor/common/core/position'; import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; @@ -42,10 +43,10 @@ export class SettingsHeaderWidget extends Widget implements IViewZone { private id: number; private _domNode: HTMLElement; - private titleContainer: HTMLElement; + protected titleContainer: HTMLElement; private messageElement: HTMLElement; - constructor(private editor: ICodeEditor, private title: string) { + constructor(protected editor: ICodeEditor, private title: string) { super(); this.create(); this._register(this.editor.onDidChangeConfiguration(() => this.layout())); @@ -64,7 +65,7 @@ export class SettingsHeaderWidget extends Widget implements IViewZone { return 0; } - private create() { + protected create() { this._domNode = DOM.$('.settings-header-widget'); this.titleContainer = DOM.append(this._domNode, DOM.$('.title-container')); @@ -102,6 +103,38 @@ export class SettingsHeaderWidget extends Widget implements IViewZone { } } +export class DefaultSettingsHeaderWidget extends SettingsHeaderWidget { + + private linkElement: HTMLElement; + private _onClick = this._register(new Emitter()); + public onClick: Event = this._onClick.event; + + protected create() { + super.create(); + + this.linkElement = DOM.append(this.titleContainer, DOM.$('a.settings-header-fuzzy-link')); + this.linkElement.textContent = localize('defaultSettingsFuzzyPrompt', "Try fuzzy search!"); + + this.onclick(this.linkElement, e => this._onClick.fire()); + this.toggleMessage(true); + } + + public toggleMessage(hasSettings: boolean, promptFuzzy = false): 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'); + } + } + } +} + export class SettingsGroupTitleWidget extends Widget implements IViewZone { private id: number; @@ -400,6 +433,8 @@ export class SearchWidget extends Widget { private countElement: HTMLElement; private searchContainer: HTMLElement; private inputBox: InputBox; + private fuzzyToggle: Checkbox; + private controlsDiv: HTMLElement; private _onDidChange: Emitter = this._register(new Emitter()); public readonly onDidChange: Event = this._onDidChange.event; @@ -417,10 +452,31 @@ 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.countElement = DOM.append(this.domNode, DOM.$('.settings-count-widget')); + this.controlsDiv = DOM.append(this.domNode, DOM.$('div.settings-search-controls')); + this.fuzzyToggle = this._register(new Checkbox({ + actionClassName: 'prefs-fuzzy-search-toggle', + isChecked: false, + onChange: () => { + this.inputBox.focus(); + this._onDidChange.fire(); + }, + title: localize('enableFuzzySearch', 'Enable experimental fuzzy search') + })); + DOM.append(this.controlsDiv, this.fuzzyToggle.domNode); + this._register(attachCheckboxStyler(this.fuzzyToggle, this.themeService)); + + this.countElement = DOM.append(this.controlsDiv, DOM.$('.settings-count-widget')); this._register(attachStylerCallback(this.themeService, { badgeBackground, contrastBorder }, colors => { const background = colors.badgeBackground ? colors.badgeBackground.toString() : null; const border = colors.contrastBorder ? colors.contrastBorder.toString() : null; @@ -462,10 +518,20 @@ export class SearchWidget extends Widget { this.countElement.textContent = message; this.inputBox.inputElement.setAttribute('aria-label', message); DOM.toggleClass(this.countElement, 'no-results', count === 0); - this.inputBox.inputElement.style.paddingRight = DOM.getTotalWidth(this.countElement) + 20 + 'px'; + this.inputBox.inputElement.style.paddingRight = this.getControlsWidth() + 'px'; this.styleCountElementForeground(); } + 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); @@ -478,10 +544,14 @@ export class SearchWidget extends Widget { this.inputBox.inputElement.style.paddingRight = '0px'; } else { DOM.removeClass(this.countElement, 'hide'); - this.inputBox.inputElement.style.paddingRight = DOM.getTotalWidth(this.countElement) + 20 + 'px'; + this.inputBox.inputElement.style.paddingRight = this.getControlsWidth() + 'px'; } } + private getControlsWidth(): number { + return DOM.getTotalWidth(this.countElement) + DOM.getTotalWidth(this.fuzzyToggle.domNode) + 20; + } + public focus() { this.inputBox.focus(); if (this.getValue()) { diff --git a/src/vs/workbench/parts/preferences/common/preferences.ts b/src/vs/workbench/parts/preferences/common/preferences.ts index 4bd6f7acdefe2..05c4ff4ffe150 100644 --- a/src/vs/workbench/parts/preferences/common/preferences.ts +++ b/src/vs/workbench/parts/preferences/common/preferences.ts @@ -9,12 +9,23 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IEditor, Position, IEditorOptions } from 'vs/platform/editor/common/editor'; +import { IModel } from 'vs/editor/common/editorCommon'; import { IKeybindingItemEntry } from 'vs/workbench/parts/preferences/common/keybindingsEditorModel'; import { IRange } from 'vs/editor/common/core/range'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { join } from 'vs/base/common/paths'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +export interface IWorkbenchSettingsConfiguration { + workbench: { + settings: { + openDefaultSettings: boolean; + experimentalFuzzySearchEndpoint: string; + experimentalFuzzySearchKey: string; + } + }; +} + export interface ISettingsGroup { id: string; range: IRange; @@ -42,22 +53,34 @@ export interface ISetting { } export interface IFilterResult { + query: string; filteredGroups: ISettingsGroup[]; allGroups: ISettingsGroup[]; matches: IRange[]; + fuzzySearchAvailable?: boolean; + metadata?: IFilterMetadata; +} + +export interface IFilterMetadata { + remoteUrl: string; + timestamp: number; + duration: number; } export interface IPreferencesEditorModel { uri: URI; - content: string; getPreference(key: string): T; 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, mostRelevantSettings?: string[]): IFilterResult; + findValueMatches(filter: string, setting: ISetting): IRange[]; } export interface IKeybindingsEditorModel extends IPreferencesEditorModel { @@ -68,13 +91,11 @@ export const IPreferencesService = createDecorator('prefere export interface IPreferencesService { _serviceBrand: any; - defaultSettingsResource: URI; - defaultResourceSettingsResource: URI; userSettingsResource: URI; workspaceSettingsResource: URI; getFolderSettingsResource(resource: URI): URI; - resolveContent(uri: URI): TPromise; + resolveModel(uri: URI): TPromise; createPreferencesEditorModel(uri: URI): TPromise>; openGlobalSettings(options?: IEditorOptions, position?: Position): TPromise; diff --git a/src/vs/workbench/parts/preferences/common/preferencesContribution.ts b/src/vs/workbench/parts/preferences/common/preferencesContribution.ts index 665bb23210484..b9021dc6437ac 100644 --- a/src/vs/workbench/parts/preferences/common/preferencesContribution.ts +++ b/src/vs/workbench/parts/preferences/common/preferencesContribution.ts @@ -120,15 +120,7 @@ export class PreferencesContribution implements IWorkbenchContribution { return TPromise.as(schemaModel); } } - return this.preferencesService.resolveContent(uri) - .then(content => { - if (content !== null && content !== void 0) { - let mode = this.modeService.getOrCreateMode('json'); - const model = this.modelService.createModel(content, mode, uri); - return TPromise.as(model); - } - return null; - }); + return this.preferencesService.resolveModel(uri); } }); } diff --git a/src/vs/workbench/parts/preferences/common/preferencesModels.ts b/src/vs/workbench/parts/preferences/common/preferencesModels.ts index 1233bd691998f..90ddb2828d2ef 100644 --- a/src/vs/workbench/parts/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/parts/preferences/common/preferencesModels.ts @@ -4,150 +4,27 @@ *--------------------------------------------------------------------------------------------*/ 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 { tail } 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 { 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 { IRange, Range } from 'vs/editor/common/core/range'; import { ITextFileService, StateChange } from 'vs/workbench/services/textfile/common/textfiles'; import { TPromise } from 'vs/base/common/winjs.base'; import { Queue } from 'vs/base/common/async'; import { IFileService } from 'vs/platform/files/common/files'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; - -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 - }; - } -} - +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; export abstract class AbstractSettingsModel extends EditorModel { @@ -155,12 +32,15 @@ export abstract class AbstractSettingsModel extends EditorModel { 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, allGroups, - matches: [] + matches: [], + query: filter }; } @@ -169,24 +49,27 @@ export abstract class AbstractSettingsModel extends EditorModel { return { filteredGroups: [group], allGroups, - matches: [] + matches: [], + query: filter }; } 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({ @@ -206,7 +89,7 @@ export abstract class AbstractSettingsModel extends EditorModel { }); } } - return { filteredGroups, matches, allGroups }; + return { filteredGroups, matches, allGroups, query: filter }; } private filterByGroupTerm(filter: string): ISettingsGroup { @@ -232,7 +115,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 +153,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 +169,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[] = []; @@ -592,18 +475,16 @@ export class WorkspaceConfigModel extends SettingsEditorModel implements ISettin } } -export class DefaultSettingsEditorModel extends AbstractSettingsModel implements ISettingsEditorModel { +export class DefaultSettingsModel { private _allSettingsGroups: ISettingsGroup[]; private _content: string; - private _contentByLines: string[]; - - constructor(private _uri: URI, private _mostCommonlyUsedSettingsKeys: string[], readonly configurationScope: ConfigurationScope) { - super(); - } + private _settingsByName: Map; - public get uri(): URI { - return this._uri; + constructor( + private _mostCommonlyUsedSettingsKeys: string[], + readonly configurationScope: ConfigurationScope, + ) { } public get content(): string { @@ -620,46 +501,29 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements return this._allSettingsGroups; } - public get mostCommonlyUsedSettings(): ISettingsGroup { - return this.settingsGroups[0]; - } - - public filterSettings(filter: string): IFilterResult { - return this.doFilterSettings(filter, this.settingsGroups); - } - - public getPreference(key: string): ISetting { - for (const group of this.settingsGroups) { - for (const section of group.sections) { - for (const setting of section.settings) { - if (setting.key === key) { - return setting; - } - } - } - } - return null; - } - private parse() { const configurations = Registry.as(Extensions.Configuration).getConfigurations().slice(); const settingsGroups = this.removeEmptySettingsGroups(configurations.sort(this.compareConfigurationNodes).reduce((result, config, index, array) => this.parseConfig(config, result, array), [])); + this.initAllSettingsMap(settingsGroups); const mostCommonlyUsed = this.getMostCommonlyUsedSettings(settingsGroups); this._allSettingsGroups = [mostCommonlyUsed, ...settingsGroups]; this._content = this.toContent(mostCommonlyUsed, settingsGroups); } - private getMostCommonlyUsedSettings(allSettingsGroups: ISettingsGroup[]): ISettingsGroup { - const map: Map = new Map(); + private initAllSettingsMap(allSettingsGroups: ISettingsGroup[]): void { + this._settingsByName = new Map(); for (const group of allSettingsGroups) { for (const section of group.sections) { for (const setting of section.settings) { - map.set(setting.key, setting); + this._settingsByName.set(setting.key, setting); } } } + } + + private getMostCommonlyUsedSettings(allSettingsGroups: ISettingsGroup[]): ISettingsGroup { const settings = this._mostCommonlyUsedSettingsKeys.map(key => { - const setting = map.get(key); + const setting = this._settingsByName.get(key); if (setting) { return { description: setting.description, @@ -773,25 +637,171 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements } private toContent(mostCommonlyUsed: ISettingsGroup, settingsGroups: ISettingsGroup[]): string { + const builder = new SettingsContentBuilder(); + builder.pushLine('['); + builder.pushGroups([mostCommonlyUsed]); + builder.pushLine(','); + builder.pushGroups(settingsGroups); + builder.pushLine(']'); + return builder.getContent(); + } + +} + +export class DefaultSettingsEditorModel extends AbstractSettingsModel implements ISettingsEditorModel { + + private _model: IModel; + private _settingsByName: Map; + private _mostRelevantLineOffset: number; + + constructor( + private _uri: URI, + reference: IReference, + readonly configurationScope: ConfigurationScope, + readonly settingsGroups: ISettingsGroup[] + ) { + super(); + this._model = reference.object.textEditorModel; + this._register(this.onDispose(() => reference.dispose())); + + this.initAllSettingsMap(); + this._mostRelevantLineOffset = tail(this.settingsGroups).range.endLineNumber + 2; + } + + public get uri(): URI { + return this._uri; + } + + public filterSettings(filter: string, groupFilter: IGroupFilter, settingFilter: ISettingFilter, mostRelevantSettings?: string[]): IFilterResult { + if (mostRelevantSettings) { + const builder = new SettingsContentBuilder(this._mostRelevantLineOffset - 1); + builder.pushLine(','); + const mostRelevantGroup = this.getMostRelevantSettings(mostRelevantSettings); + builder.pushGroups([mostRelevantGroup]); + builder.pushLine(''); + + // note: 1-indexed line numbers here + const mostRelevantContent = builder.getContent(); + const mostRelevantEndLine = this._model.getLineCount(); + this._model.applyEdits([ + { + text: mostRelevantContent, + forceMoveMarkers: false, + range: new Range(this._mostRelevantLineOffset, 1, mostRelevantEndLine, 1), + identifier: { major: 1, minor: 0 } + } + ]); + + return { + allGroups: [...this.settingsGroups, mostRelevantGroup], + filteredGroups: mostRelevantGroup.sections[0].settings.length ? [mostRelevantGroup] : [], + matches: [], + query: filter + }; + } else { + // local + return this.doFilterSettings(filter, groupFilter, settingFilter); + } + } + + public findValueMatches(filter: string, setting: ISetting): IRange[] { + return []; + } + + public getPreference(key: string): ISetting { + for (const group of this.settingsGroups) { + for (const section of group.sections) { + for (const setting of section.settings) { + if (setting.key === key) { + return setting; + } + } + } + } + return null; + } + + private initAllSettingsMap(): void { + this._settingsByName = new Map(); + for (const group of this.settingsGroups) { + for (const section of group.sections) { + for (const setting of section.settings) { + this._settingsByName.set(setting.key, setting); + } + } + } + } + + private getMostRelevantSettings(rankedSettingNames: string[]): ISettingsGroup { + const settings = rankedSettingNames.map(key => { + const setting = this._settingsByName.get(key); + if (setting) { + return { + description: setting.description, + key: setting.key, + value: setting.value, + range: null, + valueRange: null, + overrides: [] + }; + } + return null; + }).filter(setting => !!setting); + + return { + id: 'mostRelevant', + range: null, + title: nls.localize('mostRelevant', "Most Relevant"), + titleRange: null, + sections: [ + { + settings + } + ] + }; + } +} + +class SettingsContentBuilder { + private _contentByLines: string[]; + + get lines(): string[] { + return this._contentByLines; + } + + private get lineCountWithOffset(): number { + return this._contentByLines.length + this._rangeOffset; + } + + private get lastLine(): string { + return this._contentByLines[this._contentByLines.length - 1] || ''; + } + + constructor(private _rangeOffset = 0, private _maxLines = Infinity) { this._contentByLines = []; - this._contentByLines.push('['); - this.pushGroups([mostCommonlyUsed]); - this._contentByLines.push(','); - this.pushGroups(settingsGroups); - this._contentByLines.push(']'); - return this._contentByLines.join('\n'); } - private pushGroups(settingsGroups: ISettingsGroup[]): void { + private offsetIndexToIndex(offsetIdx: number): number { + return offsetIdx - this._rangeOffset; + } + + pushLine(...lineText: string[]): void { + this._contentByLines.push(...lineText); + } + + pushGroups(settingsGroups: ISettingsGroup[]): void { let lastSetting: ISetting = null; this._contentByLines.push('{'); this._contentByLines.push(''); for (const group of settingsGroups) { + this._contentByLines.push(''); lastSetting = this.pushGroup(group); } if (lastSetting) { - const content = this._contentByLines[lastSetting.range.endLineNumber - 2]; - this._contentByLines[lastSetting.range.endLineNumber - 2] = content.substring(0, content.length - 1); + // 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('}'); } @@ -799,13 +809,12 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements private pushGroup(group: ISettingsGroup): ISetting { const indent = ' '; let lastSetting: ISetting = null; - this._contentByLines.push(''); - let groupStart = this._contentByLines.length + 1; + let groupStart = this.lineCountWithOffset + 1; for (const section of group.sections) { if (section.title) { - let sectionTitleStart = this._contentByLines.length + 1; + let sectionTitleStart = this.lineCountWithOffset + 1; this.addDescription([section.title], indent, this._contentByLines); - section.titleRange = { startLineNumber: sectionTitleStart, startColumn: 1, endLineNumber: this._contentByLines.length, endColumn: this._contentByLines[this._contentByLines.length - 1].length }; + section.titleRange = { startLineNumber: sectionTitleStart, startColumn: 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length }; } if (section.settings.length) { @@ -813,38 +822,39 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements this.pushSetting(setting, indent); lastSetting = setting; } - } else { - this._contentByLines.push('// ' + nls.localize('noSettings', "No Settings")); - this._contentByLines.push(''); } } - group.range = { startLineNumber: groupStart, startColumn: 1, endLineNumber: this._contentByLines.length, endColumn: this._contentByLines[this._contentByLines.length - 1].length }; + group.range = { startLineNumber: groupStart, startColumn: 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length }; return lastSetting; } + getContent(): string { + return this._contentByLines.join('\n'); + } + private pushSetting(setting: ISetting, indent: string): void { - const settingStart = this._contentByLines.length + 1; + const settingStart = this.lineCountWithOffset + 1; setting.descriptionRanges = []; const descriptionPreValue = indent + '// '; for (const line of setting.description) { this._contentByLines.push(descriptionPreValue + line); - setting.descriptionRanges.push({ startLineNumber: this._contentByLines.length, startColumn: this._contentByLines[this._contentByLines.length - 1].indexOf(line) + 1, endLineNumber: this._contentByLines.length, endColumn: this._contentByLines[this._contentByLines.length - 1].length }); + setting.descriptionRanges.push({ startLineNumber: this.lineCountWithOffset, startColumn: this.lastLine.indexOf(line) + 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length }); } let preValueConent = indent; const keyString = JSON.stringify(setting.key); preValueConent += keyString; - setting.keyRange = { startLineNumber: this._contentByLines.length + 1, startColumn: preValueConent.indexOf(setting.key) + 1, endLineNumber: this._contentByLines.length + 1, endColumn: setting.key.length }; + setting.keyRange = { startLineNumber: this.lineCountWithOffset + 1, startColumn: preValueConent.indexOf(setting.key) + 1, endLineNumber: this.lineCountWithOffset + 1, endColumn: setting.key.length }; preValueConent += ': '; - const valueStart = this._contentByLines.length + 1; + const valueStart = this.lineCountWithOffset + 1; this.pushValue(setting, preValueConent, indent); - setting.valueRange = { startLineNumber: valueStart, startColumn: preValueConent.length + 1, endLineNumber: this._contentByLines.length, endColumn: this._contentByLines[this._contentByLines.length - 1].length + 1 }; + setting.valueRange = { startLineNumber: valueStart, startColumn: preValueConent.length + 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length + 1 }; this._contentByLines[this._contentByLines.length - 1] += ','; this._contentByLines.push(''); - setting.range = { startLineNumber: settingStart, startColumn: 1, endLineNumber: this._contentByLines.length, endColumn: this._contentByLines[this._contentByLines.length - 1].length }; + setting.range = { startLineNumber: settingStart, startColumn: 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length }; } private pushValue(setting: ISetting, preValueConent: string, indent: string): void { @@ -877,14 +887,6 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements result.push(indent + '// ' + line); } } - - protected findValueMatches(filter: string, setting: ISetting): IRange[] { - return []; - } - - public dispose(): void { - // Not disposable - } } export function defaultKeybindingsContents(keybindingService: IKeybindingService): string { @@ -895,8 +897,16 @@ export function defaultKeybindingsContents(keybindingService: IKeybindingService export class DefaultKeybindingsEditorModel implements IKeybindingsEditorModel { private _content: string; + private _model: IModel; + + constructor(private _uri: URI, + @IKeybindingService private keybindingService: IKeybindingService, + @IModeService private modeService: IModeService, + @IModelService private modelService: IModelService) { + } - constructor(private _uri: URI, @IKeybindingService private keybindingService: IKeybindingService) { + public get model(): IModel { + return this._model; } public get uri(): URI {