From a3ec0455b2bef18e3369869298dd9432e151a4ac Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 12 Jul 2019 01:07:21 +0200 Subject: [PATCH] Fix #69111 --- .../preferences/browser/preferencesSearch.ts | 356 ++++++++++++++- .../electron-browser/preferencesSearch.ts | 431 ------------------ src/vs/workbench/workbench.main.ts | 2 +- 3 files changed, 352 insertions(+), 437 deletions(-) delete mode 100644 src/vs/workbench/contrib/preferences/electron-browser/preferencesSearch.ts diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts index 5c78ca620fa..52cb6c5e841 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ISettingsEditorModel, ISetting, ISettingsGroup, ISearchResult, IGroupFilter } from 'vs/workbench/services/preferences/common/preferences'; +import { ISettingsEditorModel, ISetting, ISettingsGroup, IFilterMetadata, ISearchResult, IGroupFilter, ISettingMatcher, IScoredResults, ISettingMatch, IRemoteSetting, IExtensionSetting } from 'vs/workbench/services/preferences/common/preferences'; import { IRange } from 'vs/editor/common/core/range'; -import { distinct } from 'vs/base/common/arrays'; +import { distinct, top } from 'vs/base/common/arrays'; import * as strings from 'vs/base/common/strings'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -13,8 +13,17 @@ import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/co import { IMatch, or, matchesContiguousSubString, matchesPrefix, matchesCamelCase, matchesWords } from 'vs/base/common/filters'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IPreferencesSearchService, ISearchProvider } from 'vs/workbench/contrib/preferences/common/preferences'; +import { IPreferencesSearchService, ISearchProvider, IWorkbenchSettingsConfiguration } from 'vs/workbench/contrib/preferences/common/preferences'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IRequestService, asJson } from 'vs/platform/request/common/request'; +import { IExtensionManagementService, ILocalExtension, IExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { canceled } from 'vs/base/common/errors'; +import { ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { nullRange } from 'vs/workbench/services/preferences/common/preferencesModels'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IStringDictionary } from 'vs/base/common/collections'; export interface IEndpointDetails { urlBase?: string; @@ -24,14 +33,58 @@ export interface IEndpointDetails { export class PreferencesSearchService extends Disposable implements IPreferencesSearchService { _serviceBrand: any; + private _installedExtensions: Promise; + constructor( - @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IExtensionEnablementService private readonly extensionEnablementService: IExtensionEnablementService ) { super(); + + // This request goes to the shared process but results won't change during a window's lifetime, so cache the results. + this._installedExtensions = this.extensionManagementService.getInstalled(ExtensionType.User).then(exts => { + // Filter to enabled extensions that have settings + return exts + .filter(ext => this.extensionEnablementService.isEnabled(ext)) + .filter(ext => ext.manifest && ext.manifest.contributes && ext.manifest.contributes.configuration) + .filter(ext => !!ext.identifier.uuid); + }); + } + + private get remoteSearchAllowed(): boolean { + const workbenchSettings = this.configurationService.getValue().workbench.settings; + if (!workbenchSettings.enableNaturalLanguageSearch) { + return false; + } + + return !!this._endpoint.urlBase; + } + + private get _endpoint(): IEndpointDetails { + const workbenchSettings = this.configurationService.getValue().workbench.settings; + if (workbenchSettings.naturalLanguageSearchEndpoint) { + return { + urlBase: workbenchSettings.naturalLanguageSearchEndpoint, + key: workbenchSettings.naturalLanguageSearchKey + }; + } else { + return { + urlBase: this.environmentService.settingsSearchUrl + }; + } } getRemoteSearchProvider(filter: string, newExtensionsOnly = false): ISearchProvider | undefined { - return undefined; + const opts: IRemoteSearchProviderOptions = { + filter, + newExtensionsOnly, + endpoint: this._endpoint + }; + + return this.remoteSearchAllowed ? this.instantiationService.createInstance(RemoteSearchProvider, opts, this._installedExtensions) : undefined; } getLocalSearchProvider(filter: string): LocalSearchProvider { @@ -93,6 +146,299 @@ export class LocalSearchProvider implements ISearchProvider { } } +interface IRemoteSearchProviderOptions { + filter: string; + endpoint: IEndpointDetails; + newExtensionsOnly: boolean; +} + +interface IBingRequestDetails { + url: string; + body?: string; + hasMoreFilters?: boolean; + extensions?: ILocalExtension[]; +} + +class RemoteSearchProvider implements ISearchProvider { + // Must keep extension filter size under 8kb. 42 filters puts us there. + private static readonly MAX_REQUEST_FILTERS = 42; + private static readonly MAX_REQUESTS = 10; + private static readonly NEW_EXTENSIONS_MIN_SCORE = 1; + + private _remoteSearchP: Promise; + + constructor(private options: IRemoteSearchProviderOptions, private installedExtensions: Promise, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IRequestService private readonly requestService: IRequestService, + @ILogService private readonly logService: ILogService + ) { + this._remoteSearchP = this.options.filter ? + Promise.resolve(this.getSettingsForFilter(this.options.filter)) : + Promise.resolve(null); + } + + searchModel(preferencesModel: ISettingsEditorModel, token?: CancellationToken): Promise { + return this._remoteSearchP.then((remoteResult) => { + if (!remoteResult) { + return null; + } + + if (token && token.isCancellationRequested) { + throw canceled(); + } + + const resultKeys = Object.keys(remoteResult.scoredResults); + const highScoreKey = top(resultKeys, (a, b) => remoteResult.scoredResults[b].score - remoteResult.scoredResults[a].score, 1)[0]; + const highScore = highScoreKey ? remoteResult.scoredResults[highScoreKey].score : 0; + const minScore = highScore / 5; + if (this.options.newExtensionsOnly) { + return this.installedExtensions.then(installedExtensions => { + const newExtsMinScore = Math.max(RemoteSearchProvider.NEW_EXTENSIONS_MIN_SCORE, minScore); + const passingScoreKeys = resultKeys + .filter(k => { + const result = remoteResult.scoredResults[k]; + const resultExtId = (result.extensionPublisher + '.' + result.extensionName).toLowerCase(); + return !installedExtensions.some(ext => ext.identifier.id.toLowerCase() === resultExtId); + }) + .filter(k => remoteResult.scoredResults[k].score >= newExtsMinScore); + + const filterMatches: ISettingMatch[] = passingScoreKeys.map(k => { + const remoteSetting = remoteResult.scoredResults[k]; + const setting = remoteSettingToISetting(remoteSetting); + return { + setting, + score: remoteSetting.score, + matches: [] // TODO + }; + }); + + return { + filterMatches, + metadata: remoteResult + }; + }); + } else { + const settingMatcher = this.getRemoteSettingMatcher(remoteResult.scoredResults, minScore, preferencesModel); + const filterMatches = preferencesModel.filterSettings(this.options.filter, group => null, settingMatcher); + return { + filterMatches, + metadata: remoteResult + }; + } + }); + } + + private async getSettingsForFilter(filter: string): Promise { + const allRequestDetails: IBingRequestDetails[] = []; + + // Only send MAX_REQUESTS requests in total just to keep it sane + for (let i = 0; i < RemoteSearchProvider.MAX_REQUESTS; i++) { + const details = await this.prepareRequest(filter, i); + allRequestDetails.push(details); + if (!details.hasMoreFilters) { + break; + } + } + + return Promise.all(allRequestDetails.map(details => this.getSettingsFromBing(details))).then(allResponses => { + // Merge all IFilterMetadata + const metadata = allResponses[0]; + metadata.requestCount = 1; + + for (const response of allResponses.slice(1)) { + metadata.requestCount++; + metadata.scoredResults = { ...metadata.scoredResults, ...response.scoredResults }; + } + + return metadata; + }); + } + + private getSettingsFromBing(details: IBingRequestDetails): Promise { + this.logService.debug(`Searching settings via ${details.url}`); + if (details.body) { + this.logService.debug(`Body: ${details.body}`); + } + + const requestType = details.body ? 'post' : 'get'; + const headers: IStringDictionary = { + 'User-Agent': 'request', + 'Content-Type': 'application/json; charset=utf-8', + }; + + if (this.options.endpoint.key) { + headers['api-key'] = this.options.endpoint.key; + } + + const start = Date.now(); + return this.requestService.request({ + type: requestType, + url: details.url, + data: details.body, + headers, + timeout: 5000 + }, CancellationToken.None).then(context => { + if (typeof context.res.statusCode === 'number' && context.res.statusCode >= 300) { + throw new Error(`${JSON.stringify(details)} returned status code: ${context.res.statusCode}`); + } + + return asJson(context); + }).then((result: any) => { + const timestamp = Date.now(); + const duration = timestamp - start; + const remoteSettings: IRemoteSetting[] = (result.value || []) + .map((r: any) => { + const key = JSON.parse(r.setting || r.Setting); + const packageId = r['packageid']; + const id = getSettingKey(key, packageId); + + const value = r['value']; + const defaultValue = value ? JSON.parse(value) : value; + + const packageName = r['packagename']; + let extensionName: string | undefined; + let extensionPublisher: string | undefined; + if (packageName && packageName.indexOf('##') >= 0) { + [extensionPublisher, extensionName] = packageName.split('##'); + } + + return { + key, + id, + defaultValue, + score: r['@search.score'], + description: JSON.parse(r['details']), + packageId, + extensionName, + extensionPublisher + }; + }); + + const scoredResults = Object.create(null); + remoteSettings.forEach(s => { + scoredResults[s.id] = s; + }); + + return { + requestUrl: details.url, + requestBody: details.body, + duration, + timestamp, + scoredResults, + context: result['@odata.context'] + }; + }); + } + + private getRemoteSettingMatcher(scoredResults: IScoredResults, minScore: number, preferencesModel: ISettingsEditorModel): ISettingMatcher { + return (setting: ISetting, group: ISettingsGroup) => { + const remoteSetting = scoredResults[getSettingKey(setting.key, group.id)] || // extension setting + scoredResults[getSettingKey(setting.key, 'core')] || // core setting + scoredResults[getSettingKey(setting.key)]; // core setting from original prod endpoint + if (remoteSetting && remoteSetting.score >= minScore) { + const settingMatches = new SettingMatches(this.options.filter, setting, false, true, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches; + return { matches: settingMatches, score: remoteSetting.score }; + } + + return null; + }; + } + + private async prepareRequest(query: string, filterPage = 0): Promise { + const verbatimQuery = query; + query = escapeSpecialChars(query); + const boost = 10; + const boostedQuery = `(${query})^${boost}`; + + // Appending Fuzzy after each word. + query = query.replace(/\ +/g, '~ ') + '~'; + + const encodedQuery = encodeURIComponent(boostedQuery + ' || ' + query); + let url = `${this.options.endpoint.urlBase}`; + + if (this.options.endpoint.key) { + url += `${API_VERSION}&${QUERY_TYPE}`; + } + + const extensions = await this.installedExtensions; + const filters = this.options.newExtensionsOnly ? + [`diminish eq 'latest'`] : + this.getVersionFilters(extensions, this.environmentService.settingsSearchBuildId); + + const filterStr = filters + .slice(filterPage * RemoteSearchProvider.MAX_REQUEST_FILTERS, (filterPage + 1) * RemoteSearchProvider.MAX_REQUEST_FILTERS) + .join(' or '); + const hasMoreFilters = filters.length > (filterPage + 1) * RemoteSearchProvider.MAX_REQUEST_FILTERS; + + const body = JSON.stringify({ + query: encodedQuery, + filters: encodeURIComponent(filterStr), + rawQuery: encodeURIComponent(verbatimQuery) + }); + + return { + url, + body, + hasMoreFilters + }; + } + + private getVersionFilters(exts: ILocalExtension[], buildNumber?: number): string[] { + // Only search extensions that contribute settings + const filters = exts + .filter(ext => ext.manifest.contributes && ext.manifest.contributes.configuration) + .map(ext => this.getExtensionFilter(ext)); + + if (buildNumber) { + filters.push(`(packageid eq 'core' and startbuildno le '${buildNumber}' and endbuildno ge '${buildNumber}')`); + } + + return filters; + } + + private getExtensionFilter(ext: ILocalExtension): string { + const uuid = ext.identifier.uuid; + const versionString = ext.manifest.version + .split('.') + .map(versionPart => strings.pad(versionPart, 10)) + .join(''); + + return `(packageid eq '${uuid}' and startbuildno le '${versionString}' and endbuildno ge '${versionString}')`; + } +} + +function getSettingKey(name: string, packageId?: string): string { + return packageId ? + packageId + '##' + name : + name; +} + +const API_VERSION = 'api-version=2016-09-01-Preview'; +const QUERY_TYPE = 'querytype=full'; + +function escapeSpecialChars(query: string): string { + return query.replace(/\./g, ' ') + .replace(/[\\/+\-&|!"~*?:(){}\[\]\^]/g, '\\$&') + .replace(/ /g, ' ') // collapse spaces + .trim(); +} + +function remoteSettingToISetting(remoteSetting: IRemoteSetting): IExtensionSetting { + return { + description: remoteSetting.description.split('\n'), + descriptionIsMarkdown: false, + descriptionRanges: [], + key: remoteSetting.key, + keyRange: nullRange, + value: remoteSetting.defaultValue, + range: nullRange, + valueRange: nullRange, + overrides: [], + extensionName: remoteSetting.extensionName, + extensionPublisher: remoteSetting.extensionPublisher + }; +} + export class SettingMatches { private readonly descriptionMatchingWords: Map = new Map(); diff --git a/src/vs/workbench/contrib/preferences/electron-browser/preferencesSearch.ts b/src/vs/workbench/contrib/preferences/electron-browser/preferencesSearch.ts deleted file mode 100644 index 63b5f0c3909..00000000000 --- a/src/vs/workbench/contrib/preferences/electron-browser/preferencesSearch.ts +++ /dev/null @@ -1,431 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ISettingsEditorModel, ISetting, ISettingsGroup, IFilterMetadata, ISearchResult, IGroupFilter, ISettingMatcher, IScoredResults, ISettingMatch, IRemoteSetting, IExtensionSetting } from 'vs/workbench/services/preferences/common/preferences'; -import { top } from 'vs/base/common/arrays'; -import * as strings from 'vs/base/common/strings'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IRequestService, asJson } from 'vs/platform/request/common/request'; -import { IExtensionManagementService, ILocalExtension, IExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IPreferencesSearchService, ISearchProvider, IWorkbenchSettingsConfiguration } from 'vs/workbench/contrib/preferences/common/preferences'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { canceled } from 'vs/base/common/errors'; -import { ExtensionType } from 'vs/platform/extensions/common/extensions'; -import { nullRange } from 'vs/workbench/services/preferences/common/preferencesModels'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { PreferencesSearchService as LocalPreferencesSearchService, SettingMatches } from 'vs/workbench/contrib/preferences/browser/preferencesSearch'; -import { IStringDictionary } from 'vs/base/common/collections'; - -export interface IEndpointDetails { - urlBase?: string; - key?: string; -} - -export class PreferencesSearchService extends LocalPreferencesSearchService implements IPreferencesSearchService { - _serviceBrand: any; - - private _installedExtensions: Promise; - - constructor( - @IInstantiationService instantiationService: IInstantiationService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IExtensionEnablementService private readonly extensionEnablementService: IExtensionEnablementService - ) { - super(instantiationService); - - // This request goes to the shared process but results won't change during a window's lifetime, so cache the results. - this._installedExtensions = this.extensionManagementService.getInstalled(ExtensionType.User).then(exts => { - // Filter to enabled extensions that have settings - return exts - .filter(ext => this.extensionEnablementService.isEnabled(ext)) - .filter(ext => ext.manifest && ext.manifest.contributes && ext.manifest.contributes.configuration) - .filter(ext => !!ext.identifier.uuid); - }); - } - - private get remoteSearchAllowed(): boolean { - const workbenchSettings = this.configurationService.getValue().workbench.settings; - if (!workbenchSettings.enableNaturalLanguageSearch) { - return false; - } - - return !!this._endpoint.urlBase; - } - - private get _endpoint(): IEndpointDetails { - const workbenchSettings = this.configurationService.getValue().workbench.settings; - if (workbenchSettings.naturalLanguageSearchEndpoint) { - return { - urlBase: workbenchSettings.naturalLanguageSearchEndpoint, - key: workbenchSettings.naturalLanguageSearchKey - }; - } else { - return { - urlBase: this.environmentService.settingsSearchUrl - }; - } - } - - getRemoteSearchProvider(filter: string, newExtensionsOnly = false): ISearchProvider | undefined { - const opts: IRemoteSearchProviderOptions = { - filter, - newExtensionsOnly, - endpoint: this._endpoint - }; - - return this.remoteSearchAllowed ? this.instantiationService.createInstance(RemoteSearchProvider, opts, this._installedExtensions) : undefined; - } -} - -export class LocalSearchProvider implements ISearchProvider { - static readonly EXACT_MATCH_SCORE = 10000; - static readonly START_SCORE = 1000; - - constructor(private _filter: string) { - // Remove " and : which are likely to be copypasted as part of a setting name. - // Leave other special characters which the user might want to search for. - this._filter = this._filter - .replace(/[":]/g, ' ') - .replace(/ /g, ' ') - .trim(); - } - - searchModel(preferencesModel: ISettingsEditorModel, token?: CancellationToken): Promise { - if (!this._filter) { - return Promise.resolve(null); - } - - let orderedScore = LocalSearchProvider.START_SCORE; // Sort is not stable - const settingMatcher = (setting: ISetting) => { - const matches = new SettingMatches(this._filter, setting, true, true, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches; - const score = this._filter === setting.key ? - LocalSearchProvider.EXACT_MATCH_SCORE : - orderedScore--; - - return matches && matches.length ? - { - matches, - score - } : - null; - }; - - const filterMatches = preferencesModel.filterSettings(this._filter, this.getGroupFilter(this._filter), settingMatcher); - if (filterMatches[0] && filterMatches[0].score === LocalSearchProvider.EXACT_MATCH_SCORE) { - return Promise.resolve({ - filterMatches: filterMatches.slice(0, 1), - exactMatch: true - }); - } else { - return Promise.resolve({ - filterMatches - }); - } - } - - private getGroupFilter(filter: string): IGroupFilter { - const regex = strings.createRegExp(filter, false, { global: true }); - return (group: ISettingsGroup) => { - return regex.test(group.title); - }; - } -} - -interface IRemoteSearchProviderOptions { - filter: string; - endpoint: IEndpointDetails; - newExtensionsOnly: boolean; -} - -interface IBingRequestDetails { - url: string; - body?: string; - hasMoreFilters?: boolean; - extensions?: ILocalExtension[]; -} - -class RemoteSearchProvider implements ISearchProvider { - // Must keep extension filter size under 8kb. 42 filters puts us there. - private static readonly MAX_REQUEST_FILTERS = 42; - private static readonly MAX_REQUESTS = 10; - private static readonly NEW_EXTENSIONS_MIN_SCORE = 1; - - private _remoteSearchP: Promise; - - constructor(private options: IRemoteSearchProviderOptions, private installedExtensions: Promise, - @IEnvironmentService private readonly environmentService: IEnvironmentService, - @IRequestService private readonly requestService: IRequestService, - @ILogService private readonly logService: ILogService - ) { - this._remoteSearchP = this.options.filter ? - Promise.resolve(this.getSettingsForFilter(this.options.filter)) : - Promise.resolve(null); - } - - searchModel(preferencesModel: ISettingsEditorModel, token?: CancellationToken): Promise { - return this._remoteSearchP.then((remoteResult) => { - if (!remoteResult) { - return null; - } - - if (token && token.isCancellationRequested) { - throw canceled(); - } - - const resultKeys = Object.keys(remoteResult.scoredResults); - const highScoreKey = top(resultKeys, (a, b) => remoteResult.scoredResults[b].score - remoteResult.scoredResults[a].score, 1)[0]; - const highScore = highScoreKey ? remoteResult.scoredResults[highScoreKey].score : 0; - const minScore = highScore / 5; - if (this.options.newExtensionsOnly) { - return this.installedExtensions.then(installedExtensions => { - const newExtsMinScore = Math.max(RemoteSearchProvider.NEW_EXTENSIONS_MIN_SCORE, minScore); - const passingScoreKeys = resultKeys - .filter(k => { - const result = remoteResult.scoredResults[k]; - const resultExtId = (result.extensionPublisher + '.' + result.extensionName).toLowerCase(); - return !installedExtensions.some(ext => ext.identifier.id.toLowerCase() === resultExtId); - }) - .filter(k => remoteResult.scoredResults[k].score >= newExtsMinScore); - - const filterMatches: ISettingMatch[] = passingScoreKeys.map(k => { - const remoteSetting = remoteResult.scoredResults[k]; - const setting = remoteSettingToISetting(remoteSetting); - return { - setting, - score: remoteSetting.score, - matches: [] // TODO - }; - }); - - return { - filterMatches, - metadata: remoteResult - }; - }); - } else { - const settingMatcher = this.getRemoteSettingMatcher(remoteResult.scoredResults, minScore, preferencesModel); - const filterMatches = preferencesModel.filterSettings(this.options.filter, group => null, settingMatcher); - return { - filterMatches, - metadata: remoteResult - }; - } - }); - } - - private async getSettingsForFilter(filter: string): Promise { - const allRequestDetails: IBingRequestDetails[] = []; - - // Only send MAX_REQUESTS requests in total just to keep it sane - for (let i = 0; i < RemoteSearchProvider.MAX_REQUESTS; i++) { - const details = await this.prepareRequest(filter, i); - allRequestDetails.push(details); - if (!details.hasMoreFilters) { - break; - } - } - - return Promise.all(allRequestDetails.map(details => this.getSettingsFromBing(details))).then(allResponses => { - // Merge all IFilterMetadata - const metadata = allResponses[0]; - metadata.requestCount = 1; - - for (const response of allResponses.slice(1)) { - metadata.requestCount++; - metadata.scoredResults = { ...metadata.scoredResults, ...response.scoredResults }; - } - - return metadata; - }); - } - - private getSettingsFromBing(details: IBingRequestDetails): Promise { - this.logService.debug(`Searching settings via ${details.url}`); - if (details.body) { - this.logService.debug(`Body: ${details.body}`); - } - - const requestType = details.body ? 'post' : 'get'; - const headers: IStringDictionary = { - 'User-Agent': 'request', - 'Content-Type': 'application/json; charset=utf-8', - }; - - if (this.options.endpoint.key) { - headers['api-key'] = this.options.endpoint.key; - } - - const start = Date.now(); - return this.requestService.request({ - type: requestType, - url: details.url, - data: details.body, - headers, - timeout: 5000 - }, CancellationToken.None).then(context => { - if (typeof context.res.statusCode === 'number' && context.res.statusCode >= 300) { - throw new Error(`${JSON.stringify(details)} returned status code: ${context.res.statusCode}`); - } - - return asJson(context); - }).then((result: any) => { - const timestamp = Date.now(); - const duration = timestamp - start; - const remoteSettings: IRemoteSetting[] = (result.value || []) - .map((r: any) => { - const key = JSON.parse(r.setting || r.Setting); - const packageId = r['packageid']; - const id = getSettingKey(key, packageId); - - const value = r['value']; - const defaultValue = value ? JSON.parse(value) : value; - - const packageName = r['packagename']; - let extensionName: string | undefined; - let extensionPublisher: string | undefined; - if (packageName && packageName.indexOf('##') >= 0) { - [extensionPublisher, extensionName] = packageName.split('##'); - } - - return { - key, - id, - defaultValue, - score: r['@search.score'], - description: JSON.parse(r['details']), - packageId, - extensionName, - extensionPublisher - }; - }); - - const scoredResults = Object.create(null); - remoteSettings.forEach(s => { - scoredResults[s.id] = s; - }); - - return { - requestUrl: details.url, - requestBody: details.body, - duration, - timestamp, - scoredResults, - context: result['@odata.context'] - }; - }); - } - - private getRemoteSettingMatcher(scoredResults: IScoredResults, minScore: number, preferencesModel: ISettingsEditorModel): ISettingMatcher { - return (setting: ISetting, group: ISettingsGroup) => { - const remoteSetting = scoredResults[getSettingKey(setting.key, group.id)] || // extension setting - scoredResults[getSettingKey(setting.key, 'core')] || // core setting - scoredResults[getSettingKey(setting.key)]; // core setting from original prod endpoint - if (remoteSetting && remoteSetting.score >= minScore) { - const settingMatches = new SettingMatches(this.options.filter, setting, false, true, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches; - return { matches: settingMatches, score: remoteSetting.score }; - } - - return null; - }; - } - - private async prepareRequest(query: string, filterPage = 0): Promise { - const verbatimQuery = query; - query = escapeSpecialChars(query); - const boost = 10; - const boostedQuery = `(${query})^${boost}`; - - // Appending Fuzzy after each word. - query = query.replace(/\ +/g, '~ ') + '~'; - - const encodedQuery = encodeURIComponent(boostedQuery + ' || ' + query); - let url = `${this.options.endpoint.urlBase}`; - - if (this.options.endpoint.key) { - url += `${API_VERSION}&${QUERY_TYPE}`; - } - - const extensions = await this.installedExtensions; - const filters = this.options.newExtensionsOnly ? - [`diminish eq 'latest'`] : - this.getVersionFilters(extensions, this.environmentService.settingsSearchBuildId); - - const filterStr = filters - .slice(filterPage * RemoteSearchProvider.MAX_REQUEST_FILTERS, (filterPage + 1) * RemoteSearchProvider.MAX_REQUEST_FILTERS) - .join(' or '); - const hasMoreFilters = filters.length > (filterPage + 1) * RemoteSearchProvider.MAX_REQUEST_FILTERS; - - const body = JSON.stringify({ - query: encodedQuery, - filters: encodeURIComponent(filterStr), - rawQuery: encodeURIComponent(verbatimQuery) - }); - - return { - url, - body, - hasMoreFilters - }; - } - - private getVersionFilters(exts: ILocalExtension[], buildNumber?: number): string[] { - // Only search extensions that contribute settings - const filters = exts - .filter(ext => ext.manifest.contributes && ext.manifest.contributes.configuration) - .map(ext => this.getExtensionFilter(ext)); - - if (buildNumber) { - filters.push(`(packageid eq 'core' and startbuildno le '${buildNumber}' and endbuildno ge '${buildNumber}')`); - } - - return filters; - } - - private getExtensionFilter(ext: ILocalExtension): string { - const uuid = ext.identifier.uuid; - const versionString = ext.manifest.version - .split('.') - .map(versionPart => strings.pad(versionPart, 10)) - .join(''); - - return `(packageid eq '${uuid}' and startbuildno le '${versionString}' and endbuildno ge '${versionString}')`; - } -} - -function getSettingKey(name: string, packageId?: string): string { - return packageId ? - packageId + '##' + name : - name; -} - -const API_VERSION = 'api-version=2016-09-01-Preview'; -const QUERY_TYPE = 'querytype=full'; - -function escapeSpecialChars(query: string): string { - return query.replace(/\./g, ' ') - .replace(/[\\/+\-&|!"~*?:(){}\[\]\^]/g, '\\$&') - .replace(/ /g, ' ') // collapse spaces - .trim(); -} - -function remoteSettingToISetting(remoteSetting: IRemoteSetting): IExtensionSetting { - return { - description: remoteSetting.description.split('\n'), - descriptionIsMarkdown: false, - descriptionRanges: [], - key: remoteSetting.key, - keyRange: nullRange, - value: remoteSetting.defaultValue, - range: nullRange, - valueRange: nullRange, - overrides: [], - extensionName: remoteSetting.extensionName, - extensionPublisher: remoteSetting.extensionPublisher - }; -} \ No newline at end of file diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index f039eb03b56..1d4bb760313 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -200,7 +200,7 @@ import 'vs/workbench/contrib/localizations/browser/localizations.contribution'; import 'vs/workbench/contrib/preferences/browser/preferences.contribution'; import 'vs/workbench/contrib/preferences/browser/keybindingsEditorContribution'; import { IPreferencesSearchService } from 'vs/workbench/contrib/preferences/common/preferences'; -import { PreferencesSearchService } from 'vs/workbench/contrib/preferences/electron-browser/preferencesSearch'; +import { PreferencesSearchService } from 'vs/workbench/contrib/preferences/browser/preferencesSearch'; registerSingleton(IPreferencesSearchService, PreferencesSearchService, true); // Logs