preferencesSearch.ts 14.0 KB
Newer Older
R
Rob Lourens 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  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';
7
import Event, { Emitter } from 'vs/base/common/event';
8
import { ISettingsEditorModel, IFilterResult, ISetting, ISettingsGroup, IWorkbenchSettingsConfiguration, IFilterMetadata } from 'vs/workbench/parts/preferences/common/preferences';
R
Rob Lourens 已提交
9 10 11 12 13 14 15
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';
16
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
R
Rob Lourens 已提交
17
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
R
Rob Lourens 已提交
18

19 20
export interface IEndpointDetails {
	urlBase: string;
21
	key?: string;
22 23
}

24
export class PreferencesSearchProvider {
25 26
	private _onRemoteSearchEnablementChanged = new Emitter<boolean>();
	public onRemoteSearchEnablementChanged: Event<boolean> = this._onRemoteSearchEnablementChanged.event;
R
Rob Lourens 已提交
27

R
Rob Lourens 已提交
28 29 30 31
	constructor(
		@IWorkspaceConfigurationService private configurationService: IWorkspaceConfigurationService,
		@IEnvironmentService private environmentService: IEnvironmentService
	) {
32
		configurationService.onDidChangeConfiguration(() => this._onRemoteSearchEnablementChanged.fire(this.remoteSearchAllowed));
R
Rob Lourens 已提交
33 34
	}

35
	get remoteSearchAllowed(): boolean {
R
Rob Lourens 已提交
36 37 38 39
		if (this.environmentService.appQuality === 'stable') {
			return false;
		}

40 41 42 43 44 45
		const workbenchSettings = this.configurationService.getValue<IWorkbenchSettingsConfiguration>().workbench.settings;
		if (!workbenchSettings.enableNaturalLanguageSearch) {
			return false;
		}

		return !!this.endpoint.urlBase;
46 47 48
	}

	get endpoint(): IEndpointDetails {
49
		const workbenchSettings = this.configurationService.getValue<IWorkbenchSettingsConfiguration>().workbench.settings;
50 51 52 53 54 55 56 57 58 59
		if (workbenchSettings.experimentalFuzzySearchEndpoint) {
			return {
				urlBase: workbenchSettings.experimentalFuzzySearchEndpoint,
				key: workbenchSettings.experimentalFuzzySearchKey
			};
		} else {
			return {
				urlBase: this.environmentService.settingsSearchUrl
			};
		}
R
Rob Lourens 已提交
60 61
	}

62
	startSearch(filter: string, remote: boolean): PreferencesSearchModel {
63
		return new PreferencesSearchModel(this, filter, remote, this.environmentService);
R
Rob Lourens 已提交
64 65 66
	}
}

67
export class PreferencesSearchModel {
R
Rob Lourens 已提交
68 69 70
	private _localProvider: LocalSearchProvider;
	private _remoteProvider: RemoteSearchProvider;

71
	constructor(private provider: PreferencesSearchProvider, private filter: string, remote: boolean, environmentService: IEnvironmentService) {
R
Rob Lourens 已提交
72
		this._localProvider = new LocalSearchProvider(filter);
73

74
		if (remote && filter) {
75
			this._remoteProvider = new RemoteSearchProvider(filter, this.provider.endpoint, environmentService);
76
		}
R
Rob Lourens 已提交
77 78 79
	}

	filterPreferences(preferencesModel: ISettingsEditorModel): TPromise<IFilterResult> {
80 81 82 83
		if (!this.filter) {
			return TPromise.wrap(null);
		}

84 85 86 87 88
		if (this._remoteProvider) {
			return this._remoteProvider.filterPreferences(preferencesModel).then(null, err => {
				return this._localProvider.filterPreferences(preferencesModel);
			});
		} else {
R
Rob Lourens 已提交
89
			return this._localProvider.filterPreferences(preferencesModel);
90
		}
R
Rob Lourens 已提交
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
	}
}

class LocalSearchProvider {
	private _filter: string;

	constructor(filter: string) {
		this._filter = filter;
	}

	filterPreferences(preferencesModel: ISettingsEditorModel): TPromise<IFilterResult> {
		const regex = strings.createRegExp(this._filter, false, { global: true });

		const groupFilter = (group: ISettingsGroup) => {
			return regex.test(group.title);
		};

		const settingFilter = (setting: ISetting) => {
			return new SettingMatches(this._filter, setting, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches;
		};

		return TPromise.wrap(preferencesModel.filterSettings(this._filter, groupFilter, settingFilter));
	}
}

class RemoteSearchProvider {
	private _filter: string;
118
	private _remoteSearchP: TPromise<IFilterMetadata>;
R
Rob Lourens 已提交
119

120
	constructor(filter: string, endpoint: IEndpointDetails, private environmentService: IEnvironmentService) {
R
Rob Lourens 已提交
121
		this._filter = filter;
122
		this._remoteSearchP = filter ? this.getSettingsFromBing(filter, endpoint) : TPromise.wrap(null);
R
Rob Lourens 已提交
123 124 125
	}

	filterPreferences(preferencesModel: ISettingsEditorModel): TPromise<IFilterResult> {
126
		return this._remoteSearchP.then(remoteResult => {
R
Rob Lourens 已提交
127
			const settingFilter = (setting: ISetting) => {
128
				if (!!remoteResult.scoredResults[setting.key]) {
R
Rob Lourens 已提交
129 130 131 132 133 134 135 136 137 138 139
					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;
				}
			};

140
			if (remoteResult) {
141 142 143 144 145 146
				let sortedNames = Object.keys(remoteResult.scoredResults).sort((a, b) => remoteResult.scoredResults[b] - remoteResult.scoredResults[a]);
				if (sortedNames.length) {
					const highScore = remoteResult.scoredResults[sortedNames[0]];
					sortedNames = sortedNames.filter(name => remoteResult.scoredResults[name] >= highScore / 2);
				}

R
Rob Lourens 已提交
147
				const result = preferencesModel.filterSettings(this._filter, group => null, settingFilter, sortedNames);
148
				result.metadata = remoteResult;
149 150 151 152
				return result;
			} else {
				return null;
			}
R
Rob Lourens 已提交
153 154 155
		});
	}

156 157 158 159 160 161 162 163 164
	private getSettingsFromBing(filter: string, endpoint: IEndpointDetails): TPromise<IFilterMetadata> {
		const url = prepareUrl(filter, endpoint, this.environmentService.settingsSearchBuildId);
		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
			})
165
		})
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
			.then(r => r.json())
			.then(result => {
				const timestamp = Date.now();
				const duration = timestamp - start;
				const suggestions = (result.value || [])
					.map(r => ({
						name: r.setting || r.Setting,
						score: r['@search.score']
					}));

				const scoredResults = Object.create(null);
				suggestions.forEach(s => {
					const name = s.name
						.replace(/^"/, '')
						.replace(/"$/, '');
					scoredResults[name] = s.score;
				});

				return <IFilterMetadata>{
					remoteUrl: url,
					duration,
					timestamp,
					scoredResults
				};
R
Rob Lourens 已提交
190 191
			});

192 193
		return TPromise.as(p as any);
	}
R
Rob Lourens 已提交
194 195
}

196
const API_VERSION = 'api-version=2016-09-01-Preview';
R
Rob Lourens 已提交
197
const QUERY_TYPE = 'querytype=full';
198
const SCORING_PROFILE = 'scoringProfile=ranking';
R
Rob Lourens 已提交
199 200 201 202 203 204 205 206

function escapeSpecialChars(query: string): string {
	return query.replace(/\./g, ' ')
		.replace(/[\\/+\-&|!"~*?:(){}\[\]\^]/g, '\\$&')
		.replace(/  /g, ' ') // collapse spaces
		.trim();
}

207
function prepareUrl(query: string, endpoint: IEndpointDetails, buildNumber: number): string {
R
Rob Lourens 已提交
208
	query = escapeSpecialChars(query);
209
	const boost = 10;
210
	const userQuery = `(${query})^${boost}`;
R
Rob Lourens 已提交
211 212 213 214

	// Appending Fuzzy after each word.
	query = query.replace(/\ +/g, '~ ') + '~';

215 216 217 218
	let url = `${endpoint.urlBase}?search=${encodeURIComponent(userQuery + ' || ' + query)}`;
	if (endpoint.key) {
		url += `&${API_VERSION}&${QUERY_TYPE}&${SCORING_PROFILE}`;
	}
219 220 221 222
	if (buildNumber) {
		url += `&$filter startbuildno le ${buildNumber} and endbuildno ge ${buildNumber}`;
	}

223
	return url + `&$filter=startbuildno le 119000227 and endbuildno ge 119000227`;
R
Rob Lourens 已提交
224
}
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345

class SettingMatches {

	private readonly descriptionMatchingWords: Map<string, IRange[]> = new Map<string, IRange[]>();
	private readonly keyMatchingWords: Map<string, IRange[]> = new Map<string, IRange[]>();
	private readonly valueMatchingWords: Map<string, IRange[]> = new Map<string, IRange[]>();

	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<IConfigurationRegistry>(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<string, IRange[]>, others: Map<string, IRange[]>[]): 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
		};
	}
}