提交 7fbd3387 编写于 作者: R Rob Lourens

Merge branch 'roblou/newExtensionSettingSearch'

......@@ -1692,6 +1692,17 @@ CommandsRegistry.registerCommand('workbench.extensions.action.showExtensionsForL
});
});
CommandsRegistry.registerCommand('workbench.extensions.action.showExtensionsWithId', function (accessor: ServicesAccessor, extensionId: string) {
const viewletService = accessor.get(IViewletService);
return viewletService.openViewlet(VIEWLET_ID, true)
.then(viewlet => viewlet as IExtensionsViewlet)
.then(viewlet => {
viewlet.search(`@id:${extensionId}`);
viewlet.focus();
});
});
export const extensionButtonProminentBackground = registerColor('extensionButton.prominentBackground', {
dark: '#327e36',
light: '#327e36',
......
......@@ -111,7 +111,7 @@ export class PreferencesEditor extends BaseEditor {
private preferencesRenderers: PreferencesRenderersController;
private delayedFilterLogging: Delayer<void>;
private remoteSearchThrottle: ThrottledDelayer<IFilterOrSearchResult>;
private remoteSearchThrottle: ThrottledDelayer<void>;
private _lastReportedFilter: string;
private lastFocusedWidget: SearchWidget | SideBySidePreferencesWidget = null;
......@@ -243,18 +243,18 @@ export class PreferencesEditor extends BaseEditor {
TPromise.join([
this.preferencesRenderers.localFilterPreferences(query),
this.triggerThrottledSearch(query)
]).then(results => {
if (results) {
const [localResult, remoteResult] = results;
]).then(() => {
const result = this.preferencesRenderers.lastFilterResult;
if (result) {
this.delayedFilterLogging.trigger(() => this.reportFilteringUsed(
query,
remoteResult ? remoteResult.defaultSettingsGroupCounts : localResult.defaultSettingsGroupCounts,
remoteResult && remoteResult.metadata));
result.defaultSettingsGroupCounts,
result.metadata));
}
});
}
private triggerThrottledSearch(query: string): TPromise<IFilterOrSearchResult> {
private triggerThrottledSearch(query: string): TPromise<void> {
if (query) {
return this.remoteSearchThrottle.trigger(() => this.preferencesRenderers.remoteSearchPreferences(query));
} else {
......@@ -364,11 +364,13 @@ class PreferencesRenderersController extends Disposable {
private _editablePreferencesRendererDisposables: IDisposable[] = [];
private _settingsNavigator: SettingsNavigator;
private _filtersInProgress: TPromise<IFilterResult>[];
private _remoteFilterInProgress: TPromise<any>;
private _currentLocalSearchProvider: ISearchProvider;
private _currentRemoteSearchProvider: ISearchProvider;
private _currentNewExtensionsSearchProvider: ISearchProvider;
private _lastQuery: string;
private _lastFilterResult: IFilterOrSearchResult;
private _onDidFilterResultsCountChange: Emitter<number> = this._register(new Emitter<number>());
public onDidFilterResultsCountChange: Event<number> = this._onDidFilterResultsCountChange.event;
......@@ -380,6 +382,10 @@ class PreferencesRenderersController extends Disposable {
super();
}
get lastFilterResult(): IFilterOrSearchResult {
return this._lastFilterResult;
}
get defaultPreferencesRenderer(): IPreferencesRenderer<ISetting> {
return this._defaultPreferencesRenderer;
}
......@@ -413,34 +419,47 @@ class PreferencesRenderersController extends Disposable {
}
}
async _onEditableContentDidChange(): TPromise<void> {
private async _onEditableContentDidChange(): TPromise<void> {
await this.localFilterPreferences(this._lastQuery, true);
await this.remoteSearchPreferences(this._lastQuery, true);
}
remoteSearchPreferences(query: string, updateCurrentResults?: boolean): TPromise<IFilterOrSearchResult> {
remoteSearchPreferences(query: string, updateCurrentResults?: boolean): TPromise<void> {
if (this._remoteFilterInProgress && this._remoteFilterInProgress.cancel) {
// Resolved/rejected promises have no .cancel()
this._remoteFilterInProgress.cancel();
}
this._currentRemoteSearchProvider = (updateCurrentResults && this._currentRemoteSearchProvider) || this.preferencesSearchService.getRemoteSearchProvider(query);
return this.filterOrSearchPreferences(query, this._currentRemoteSearchProvider, 'nlpResult', nls.localize('nlpResult', "Natural Language Results"));
this._currentNewExtensionsSearchProvider = (updateCurrentResults && this._currentNewExtensionsSearchProvider) || this.preferencesSearchService.getRemoteSearchProvider(query, true);
this._remoteFilterInProgress = this.filterOrSearchPreferences(query, this._currentRemoteSearchProvider, 'nlpResult', nls.localize('nlpResult', "Natural Language Results"), 1)
.then(result => this.filterOrSearchPreferences(query, this._currentNewExtensionsSearchProvider, 'newExtensionsResult', nls.localize('newExtensionsResult', "Other Extension Results"), 2));
return this._remoteFilterInProgress.then(() => {
this._remoteFilterInProgress = null;
}, err => {
if (isPromiseCanceledError(err)) {
return null;
} else {
onUnexpectedError(err);
}
});
}
localFilterPreferences(query: string, updateCurrentResults?: boolean): TPromise<IFilterOrSearchResult> {
localFilterPreferences(query: string, updateCurrentResults?: boolean): TPromise<void> {
this._currentLocalSearchProvider = (updateCurrentResults && this._currentLocalSearchProvider) || this.preferencesSearchService.getLocalSearchProvider(query);
return this.filterOrSearchPreferences(query, this._currentLocalSearchProvider, 'filterResult', nls.localize('filterResult', "Filtered Results"));
return this.filterOrSearchPreferences(query, this._currentLocalSearchProvider, 'filterResult', nls.localize('filterResult', "Filtered Results"), 0);
}
filterOrSearchPreferences(query: string, searchProvider: ISearchProvider, groupId: string, groupLabel: string): TPromise<IFilterOrSearchResult> {
private filterOrSearchPreferences(query: string, searchProvider: ISearchProvider, groupId: string, groupLabel: string, groupOrder: number): TPromise<void> {
this._lastQuery = query;
if (this._filtersInProgress) {
// Resolved/rejected promises have no .cancel()
this._filtersInProgress.forEach(p => p.cancel && p.cancel());
}
this._filtersInProgress = [
this._filterOrSearchPreferences(query, this.defaultPreferencesRenderer, searchProvider, groupId, groupLabel),
this._filterOrSearchPreferences(query, this.editablePreferencesRenderer, searchProvider, groupId, groupLabel)];
const filterPs = [
this._filterOrSearchPreferences(query, this.defaultPreferencesRenderer, searchProvider, groupId, groupLabel, groupOrder),
this._filterOrSearchPreferences(query, this.editablePreferencesRenderer, searchProvider, groupId, groupLabel, groupOrder)];
return TPromise.join(this._filtersInProgress).then(results => {
this._filtersInProgress = null;
return TPromise.join(filterPs).then(results => {
const [defaultFilterResult, editableFilterResult] = results;
this.consolidateAndUpdate(defaultFilterResult, editableFilterResult);
......@@ -449,13 +468,7 @@ class PreferencesRenderersController extends Disposable {
defaultSettingsGroupCounts: defaultFilterResult && this._countById(defaultFilterResult.filteredGroups)
};
return result;
}, err => {
if (isPromiseCanceledError(err)) {
return null;
} else {
onUnexpectedError(err);
}
this._lastFilterResult = result;
});
}
......@@ -469,7 +482,7 @@ class PreferencesRenderersController extends Disposable {
this._focusPreference(setting, this._editablePreferencesRenderer);
}
private _filterOrSearchPreferences(filter: string, preferencesRenderer: IPreferencesRenderer<ISetting>, provider: ISearchProvider, groupId: string, groupLabel: string): TPromise<IFilterResult> {
private _filterOrSearchPreferences(filter: string, preferencesRenderer: IPreferencesRenderer<ISetting>, provider: ISearchProvider, groupId: string, groupLabel: string, groupOrder: number): TPromise<IFilterResult> {
if (preferencesRenderer) {
const model = <ISettingsEditorModel>preferencesRenderer.preferencesModel;
const searchP = provider ? provider.searchModel(model) : TPromise.wrap(null);
......@@ -493,7 +506,8 @@ class PreferencesRenderersController extends Disposable {
model.updateResultGroup(groupId, {
id: groupId,
label: groupLabel,
result: searchResult
result: searchResult,
order: groupOrder
}) :
model.updateResultGroup(groupId, null);
......
......@@ -6,6 +6,7 @@
import { TPromise } from 'vs/base/common/winjs.base';
import * as nls from 'vs/nls';
import { Delayer } from 'vs/base/common/async';
import * as arrays from 'vs/base/common/arrays';
import * as strings from 'vs/base/common/strings';
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IAction } from 'vs/base/common/actions';
......@@ -16,7 +17,7 @@ import * as editorCommon from 'vs/editor/common/editorCommon';
import { Range, IRange } from 'vs/editor/common/core/range';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IPreferencesService, ISettingsGroup, ISetting, IPreferencesEditorModel, IFilterResult, ISettingsEditorModel, IScoredResults, IWorkbenchSettingsConfiguration } from 'vs/workbench/parts/preferences/common/preferences';
import { IPreferencesService, ISettingsGroup, ISetting, IPreferencesEditorModel, IFilterResult, ISettingsEditorModel, IScoredResults, IWorkbenchSettingsConfiguration, IExtensionSetting } from 'vs/workbench/parts/preferences/common/preferences';
import { SettingsEditorModel, DefaultSettingsEditorModel, WorkspaceConfigurationEditorModel } 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';
......@@ -33,6 +34,8 @@ import { MarkdownString } from 'vs/base/common/htmlContent';
import { overrideIdentifierFromKey, IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ITextModel, IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model';
import { CodeLensProviderRegistry, CodeLensProvider, ICodeLensSymbol } from 'vs/editor/common/modes';
import { CancellationToken } from 'vs/base/common/cancellation';
export interface IPreferencesRenderer<T> extends IDisposable {
readonly preferencesModel: IPreferencesEditorModel<T>;
......@@ -254,6 +257,7 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR
private editSettingActionRenderer: EditSettingRenderer;
private feedbackWidgetRenderer: FeedbackWidgetRenderer;
private bracesHidingRenderer: BracesHidingRenderer;
private extensionCodelensRenderer: ExtensionCodelensRenderer;
private filterResult: IFilterResult;
private _onUpdatePreference: Emitter<{ key: string, value: any, source: IIndexedSetting }> = new Emitter<{ key: string, value: any, source: IIndexedSetting }>();
......@@ -278,6 +282,7 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR
this.feedbackWidgetRenderer = this._register(instantiationService.createInstance(FeedbackWidgetRenderer, editor));
this.bracesHidingRenderer = this._register(instantiationService.createInstance(BracesHidingRenderer, editor, preferencesModel));
this.hiddenAreasRenderer = this._register(instantiationService.createInstance(HiddenAreasRenderer, editor, [this.settingsGroupTitleRenderer, this.filteredMatchesRenderer, this.bracesHidingRenderer]));
this.extensionCodelensRenderer = this._register(instantiationService.createInstance(ExtensionCodelensRenderer, editor));
this._register(this.editSettingActionRenderer.onUpdateSetting(e => this._onUpdatePreference.fire(e)));
this._register(this.settingsGroupTitleRenderer.onHiddenAreasChanged(() => this.hiddenAreasRenderer.render()));
......@@ -305,6 +310,7 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR
public filterPreferences(filterResult: IFilterResult): void {
this.filterResult = filterResult;
if (filterResult) {
this.filteredMatchesRenderer.render(filterResult, this.preferencesModel.settingsGroups);
this.settingsGroupTitleRenderer.render(filterResult.filteredGroups);
......@@ -313,6 +319,7 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR
this.settingHighlighter.clear(true);
this.bracesHidingRenderer.render(filterResult, this.preferencesModel.settingsGroups);
this.editSettingActionRenderer.render(filterResult.filteredGroups, this._associatedPreferencesModel);
this.extensionCodelensRenderer.render(filterResult);
} else {
this.settingHighlighter.clear(true);
this.filteredMatchesRenderer.render(null, this.preferencesModel.settingsGroups);
......@@ -322,6 +329,7 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR
this.settingsGroupTitleRenderer.showGroup(0);
this.bracesHidingRenderer.render(null, this.preferencesModel.settingsGroups);
this.editSettingActionRenderer.render(this.preferencesModel.settingsGroups, this._associatedPreferencesModel);
this.extensionCodelensRenderer.render(null);
}
this.hiddenAreasRenderer.render();
......@@ -615,20 +623,23 @@ export class FeedbackWidgetRenderer extends Disposable {
const result = this._currentResult;
const actualResults = result.metadata.scoredResults;
const actualResultNames = Object.keys(actualResults);
const actualResultIds = Object.keys(actualResults);
const feedbackQuery: any = {};
feedbackQuery['comment'] = FeedbackWidgetRenderer.DEFAULT_COMMENT_TEXT;
feedbackQuery['queryString'] = result.query;
feedbackQuery['resultScores'] = {};
actualResultNames.forEach(settingKey => {
feedbackQuery['resultScores'][settingKey] = 10;
actualResultIds.forEach(settingId => {
const outputKey = actualResults[settingId].key;
feedbackQuery['resultScores'][outputKey] = 10;
});
feedbackQuery['alts'] = [];
const contents = FeedbackWidgetRenderer.INSTRUCTION_TEXT + '\n' +
JSON.stringify(feedbackQuery, undefined, ' ') + '\n\n' +
actualResultNames.map(name => `// ${name}: ${result.metadata.scoredResults[name]}`).join('\n');
actualResultIds.map(name => {
return `// ${actualResults[name].key}: ${actualResults[name].score}`;
}).join('\n');
this.editorService.openEditor({ contents, language: 'jsonc' }, /*sideBySide=*/true).then(feedbackEditor => {
const sendFeedbackWidget = this._register(this.instantiationService.createInstance(FloatingClickWidget, feedbackEditor.getControl(), 'Send feedback', null));
......@@ -835,6 +846,51 @@ export class HighlightMatchesRenderer extends Disposable {
}
}
export class ExtensionCodelensRenderer extends Disposable implements CodeLensProvider {
private filterResult: IFilterResult;
constructor() {
super();
this._register(CodeLensProviderRegistry.register({ pattern: '**/settings.json' }, this));
}
public render(filterResult: IFilterResult): void {
this.filterResult = filterResult;
}
public provideCodeLenses(model: ITextModel, token: CancellationToken): ICodeLensSymbol[] {
if (!this.filterResult || !this.filterResult.filteredGroups) {
return [];
}
const newExtensionGroup = arrays.first(this.filterResult.filteredGroups, g => g.id === 'newExtensionsResult');
if (!newExtensionGroup) {
return [];
}
return newExtensionGroup.sections[0].settings
.filter((s: IExtensionSetting) => {
// Skip any non IExtensionSettings that somehow got in here
return s.extensionName && s.extensionPublisher;
})
.map((s: IExtensionSetting) => {
const extId = s.extensionPublisher + '.' + s.extensionName;
return <ICodeLensSymbol>{
command: {
title: nls.localize('newExtensionLabel', "View \"{0}\"", extId),
id: 'workbench.extensions.action.showExtensionsWithId',
arguments: [extId.toLowerCase()]
},
range: new Range(s.keyRange.startLineNumber, 1, s.keyRange.startLineNumber, 1)
};
});
}
public resolveCodeLens(model: ITextModel, codeLens: ICodeLensSymbol, token: CancellationToken): ICodeLensSymbol {
return codeLens;
}
}
export interface IIndexedSetting extends ISetting {
index: number;
groupId: string;
......
......@@ -24,6 +24,7 @@ export interface IWorkbenchSettingsConfiguration {
naturalLanguageSearchEndpoint: string;
naturalLanguageSearchKey: string;
naturalLanguageSearchAutoIngestFeedback: boolean;
useNaturalLanguageSearchPost: boolean;
enableNaturalLanguageSearch: boolean;
enableNaturalLanguageSearchFeedback: boolean;
}
......@@ -56,6 +57,11 @@ export interface ISetting {
overrideOf?: ISetting;
}
export interface IExtensionSetting extends ISetting {
extensionName: string;
extensionPublisher: string;
}
export interface ISearchResult {
filterMatches: ISettingMatch[];
metadata?: IFilterMetadata;
......@@ -65,6 +71,7 @@ export interface ISearchResultGroup {
id: string;
label: string;
result: ISearchResult;
order: number;
}
export interface IFilterResult {
......@@ -82,7 +89,18 @@ export interface ISettingMatch {
}
export interface IScoredResults {
[key: string]: number;
[key: string]: IRemoteSetting;
}
export interface IRemoteSetting {
score: number;
key: string;
id: string;
defaultValue: string;
description: string;
packageId: string;
extensionName?: string;
extensionPublisher?: string;
}
export interface IFilterMetadata {
......@@ -102,7 +120,7 @@ export interface IPreferencesEditorModel<T> {
}
export type IGroupFilter = (group: ISettingsGroup) => boolean;
export type ISettingMatcher = (setting: ISetting) => { matches: IRange[], score: number };
export type ISettingMatcher = (setting: ISetting, group: ISettingsGroup) => { matches: IRange[], score: number };
export interface ISettingsEditorModel extends IPreferencesEditorModel<ISetting> {
readonly onDidChangeGroups: Event<void>;
......@@ -177,7 +195,7 @@ export interface IPreferencesSearchService {
_serviceBrand: any;
getLocalSearchProvider(filter: string): ISearchProvider;
getRemoteSearchProvider(filter: string): ISearchProvider;
getRemoteSearchProvider(filter: string, newExtensionsOnly?: boolean): ISearchProvider;
}
export interface ISearchProvider {
......
......@@ -41,12 +41,14 @@ export abstract class AbstractSettingsModel extends EditorModel {
* Remove duplicates between result groups, preferring results in earlier groups
*/
private removeDuplicateResults(): void {
// Depends on order of map keys
const settingKeys = new Set<string>();
this._currentResultGroups.forEach((group, id) => {
group.result.filterMatches = group.result.filterMatches.filter(s => !settingKeys.has(s.setting.key));
group.result.filterMatches.forEach(s => settingKeys.add(s.setting.key));
});
map.keys(this._currentResultGroups)
.sort((a, b) => this._currentResultGroups.get(a).order - this._currentResultGroups.get(b).order)
.forEach(groupId => {
const group = this._currentResultGroups.get(groupId);
group.result.filterMatches = group.result.filterMatches.filter(s => !settingKeys.has(s.setting.key));
group.result.filterMatches.forEach(s => settingKeys.add(s.setting.key));
});
}
public filterSettings(filter: string, groupFilter: IGroupFilter, settingMatcher: ISettingMatcher): ISettingMatch[] {
......@@ -57,11 +59,11 @@ export abstract class AbstractSettingsModel extends EditorModel {
const groupMatched = groupFilter(group);
for (const section of group.sections) {
for (const setting of section.settings) {
const settingMatchResult = settingMatcher(setting);
const settingMatchResult = settingMatcher(setting, group);
if (groupMatched || settingMatchResult) {
filterMatches.push({
setting,
setting: this.copySetting(setting),
matches: settingMatchResult && settingMatchResult.matches,
score: settingMatchResult ? settingMatchResult.score : 0
});
......@@ -70,7 +72,22 @@ export abstract class AbstractSettingsModel extends EditorModel {
}
}
return filterMatches.sort((a, b) => b.score - a.score);
return filterMatches
.sort((a, b) => b.score - a.score)
.map(filteredMatch => {
// Fix match ranges to offset from setting start line
return <ISettingMatch>{
setting: filteredMatch.setting,
score: filteredMatch.score,
matches: filteredMatch.matches && filteredMatch.matches.map(match => {
return new Range(
match.startLineNumber - filteredMatch.setting.range.startLineNumber,
match.startColumn,
match.endLineNumber - filteredMatch.setting.range.startLineNumber,
match.endColumn);
})
};
});
}
public getPreference(key: string): ISetting {
......@@ -86,6 +103,18 @@ export abstract class AbstractSettingsModel extends EditorModel {
return null;
}
private copySetting(setting: ISetting): ISetting {
return <ISetting>{
description: setting.description,
key: setting.key,
value: setting.value,
range: setting.range,
overrides: [],
overrideOf: setting.overrideOf
};
}
protected get filterGroups(): ISettingsGroup[] {
return this.settingsGroups;
}
......@@ -611,7 +640,9 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements
protected update(): IFilterResult {
// Grab current result groups, only render non-empty groups
const resultGroups = map.values(this._currentResultGroups);
const resultGroups = map
.values(this._currentResultGroups)
.sort((a, b) => a.order - b.order);
const nonEmptyResultGroups = resultGroups.filter(group => group.result.filterMatches.length);
const startLine = tail(this.settingsGroups).range.endLineNumber + 2;
......@@ -665,21 +696,6 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements
}
private writeSettingsGroupToBuilder(builder: SettingsContentBuilder, settingsGroup: ISettingsGroup, filterMatches: ISettingMatch[]): IRange[] {
// Fix match ranges to offset from setting start line
filterMatches = filterMatches.map(filteredMatch => {
return <ISettingMatch>{
setting: filteredMatch.setting,
score: filteredMatch.score,
matches: filteredMatch.matches && filteredMatch.matches.map(match => {
return new Range(
match.startLineNumber - filteredMatch.setting.range.startLineNumber,
match.startColumn,
match.endLineNumber - filteredMatch.setting.range.startLineNumber,
match.endColumn);
})
};
});
builder.pushGroup(settingsGroup);
builder.pushLine(',');
......@@ -718,19 +734,6 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements
return null;
}
private copySettings(settings: ISetting[]): ISetting[] {
return settings.map(setting => {
return <ISetting>{
description: setting.description,
key: setting.key,
value: setting.value,
range: null,
valueRange: null,
overrides: []
};
});
}
private getGroup(resultGroup: ISearchResultGroup): ISettingsGroup {
return <ISettingsGroup>{
id: resultGroup.id,
......@@ -739,7 +742,7 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements
titleRange: null,
sections: [
{
settings: this.copySettings(resultGroup.result.filterMatches.map(m => m.setting))
settings: resultGroup.result.filterMatches.map(m => m.setting)
}
]
};
......
......@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { TPromise } from 'vs/base/common/winjs.base';
import { ISettingsEditorModel, ISetting, ISettingsGroup, IWorkbenchSettingsConfiguration, IFilterMetadata, IPreferencesSearchService, ISearchResult, ISearchProvider, IGroupFilter, ISettingMatcher, IScoredResults } from 'vs/workbench/parts/preferences/common/preferences';
import { ISettingsEditorModel, ISetting, ISettingsGroup, IWorkbenchSettingsConfiguration, IFilterMetadata, IPreferencesSearchService, ISearchResult, ISearchProvider, IGroupFilter, ISettingMatcher, IScoredResults, ISettingMatch, IRemoteSetting, IExtensionSetting } from 'vs/workbench/parts/preferences/common/preferences';
import { IRange } from 'vs/editor/common/core/range';
import { distinct, top } from 'vs/base/common/arrays';
import * as strings from 'vs/base/common/strings';
......@@ -19,6 +19,7 @@ import { IRequestService } from 'vs/platform/request/node/request';
import { asJson } from 'vs/base/node/request';
import { Disposable } from 'vs/base/common/lifecycle';
import { IExtensionManagementService, LocalExtensionType, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ILogService } from 'vs/platform/log/common/log';
export interface IEndpointDetails {
urlBase: string;
......@@ -67,8 +68,17 @@ export class PreferencesSearchService extends Disposable implements IPreferences
}
}
getRemoteSearchProvider(filter: string): RemoteSearchProvider {
return this.remoteSearchAllowed && this.instantiationService.createInstance(RemoteSearchProvider, filter, this._endpoint, this._installedExtensions);
getRemoteSearchProvider(filter: string, newExtensionsOnly = false): ISearchProvider {
const workbenchSettings = this.configurationService.getValue<IWorkbenchSettingsConfiguration>().workbench.settings;
const opts: IRemoteSearchProviderOptions = {
filter,
newExtensionsOnly,
endpoint: this._endpoint,
usePost: workbenchSettings.useNaturalLanguageSearchPost
};
return this.remoteSearchAllowed && this.instantiationService.createInstance(RemoteSearchProvider, opts, this._installedExtensions);
}
getLocalSearchProvider(filter: string): LocalSearchProvider {
......@@ -90,7 +100,7 @@ export class LocalSearchProvider implements ISearchProvider {
let score = 1000; // Sort is not stable
const settingMatcher = (setting: ISetting) => {
const matches = new SettingMatches(this._filter, setting, true, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches;
const matches = new SettingMatches(this._filter, setting, true, false, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches;
return matches && matches.length ?
{
matches,
......@@ -113,77 +123,124 @@ export class LocalSearchProvider implements ISearchProvider {
}
}
export class RemoteSearchProvider implements ISearchProvider {
private _filter: string;
interface IRemoteSearchProviderOptions {
filter: string;
endpoint: IEndpointDetails;
newExtensionsOnly: boolean;
usePost: boolean;
}
class RemoteSearchProvider implements ISearchProvider {
private _remoteSearchP: TPromise<IFilterMetadata>;
constructor(filter: string, endpoint: IEndpointDetails, private installedExtensions: TPromise<ILocalExtension[]>,
constructor(private options: IRemoteSearchProviderOptions, private installedExtensions: TPromise<ILocalExtension[]>,
@IEnvironmentService private environmentService: IEnvironmentService,
@IRequestService private requestService: IRequestService,
@ILogService private logService: ILogService
) {
this._filter = filter;
// @queries are always handled by local filter
this._remoteSearchP = filter && !strings.startsWith(filter, '@') ?
this.getSettingsFromBing(filter, endpoint) :
TPromise.wrap(null);
this._remoteSearchP = (this.options.newExtensionsOnly && !this.options.usePost) ? TPromise.wrap(null) :
this.options.filter ?
this.getSettingsFromBing(this.options.filter) :
TPromise.wrap(null);
}
searchModel(preferencesModel: ISettingsEditorModel): TPromise<ISearchResult> {
return this._remoteSearchP.then(remoteResult => {
if (remoteResult) {
const highScoreKey = top(Object.keys(remoteResult.scoredResults), (a, b) => remoteResult.scoredResults[b] - remoteResult.scoredResults[a], 1)[0];
const highScore = highScoreKey ? remoteResult.scoredResults[highScoreKey] : 0;
const minScore = highScore / 5;
if (!remoteResult) {
return null;
}
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) {
const passingScoreKeys = resultKeys.filter(k => remoteResult.scoredResults[k].score >= minScore);
const filterMatches: ISettingMatch[] = passingScoreKeys.map(k => {
const remoteSetting = remoteResult.scoredResults[k];
const setting = remoteSettingToISetting(remoteSetting);
return <ISettingMatch>{
setting,
score: remoteSetting.score,
matches: [] // TODO
};
});
const settingMatcher = this.getRemoteSettingMatcher(remoteResult.scoredResults, minScore, preferencesModel);
const filterMatches = preferencesModel.filterSettings(this._filter, group => null, settingMatcher);
return <ISearchResult>{
filterMatches,
metadata: remoteResult
};
} else {
return null;
const settingMatcher = this.getRemoteSettingMatcher(remoteResult.scoredResults, minScore, preferencesModel);
const filterMatches = preferencesModel.filterSettings(this.options.filter, group => null, settingMatcher);
return <ISearchResult>{
filterMatches,
metadata: remoteResult
};
}
});
}
private getSettingsFromBing(filter: string, endpoint: IEndpointDetails): TPromise<IFilterMetadata> {
private getSettingsFromBing(filter: string): TPromise<IFilterMetadata> {
const start = Date.now();
return this.prepareUrl(filter, endpoint, this.environmentService.settingsSearchBuildId).then(url => {
return this.prepareRequest(filter).then(details => {
this.logService.debug(`Searching settings via ${details.url}`);
if (details.body) {
this.logService.debug(`Body: ${details.body}`);
}
const requestType = details.body ? 'post' : 'get';
return this.requestService.request({
url,
type: requestType,
url: details.url,
data: details.body,
headers: {
'User-Agent': 'request',
'Content-Type': 'application/json; charset=utf-8',
'api-key': endpoint.key
'api-key': this.options.endpoint.key
},
timeout: 5000
}).then(context => {
if (context.res.statusCode >= 300) {
throw new Error(`${url} returned status code: ${context.res.statusCode}`);
throw new Error(`${details} returned status code: ${context.res.statusCode}`);
}
return asJson(context);
}).then((result: any) => {
const timestamp = Date.now();
const duration = timestamp - start;
const suggestions = (result.value || [])
.map(r => ({
name: r.setting || r.Setting,
score: r['@search.score']
}));
const remoteSettings: IRemoteSetting[] = (result.value || [])
.map(r => {
const key = JSON.parse(r.setting || r.Setting);
const packageId = r['packageid'];
const id = getSettingKey(key, packageId);
const packageName = r['packagename'];
let extensionName: string;
let extensionPublisher: string;
if (packageName && packageName.indexOf('##') >= 0) {
[extensionPublisher, extensionName] = packageName.split('##');
}
return <IRemoteSetting>{
key,
id,
defaultValue: r['value'],
score: r['@search.score'],
description: JSON.parse(r['details']),
packageId,
extensionName,
extensionPublisher
};
});
const scoredResults = Object.create(null);
suggestions.forEach(s => {
const name = s.name
.replace(/^"/, '')
.replace(/"$/, '');
scoredResults[name] = s.score;
remoteSettings.forEach(s => {
scoredResults[s.id] = s;
});
return <IFilterMetadata>{
remoteUrl: url,
remoteUrl: details.url, // telemetry for filter text?
duration,
timestamp,
scoredResults,
......@@ -194,18 +251,20 @@ export class RemoteSearchProvider implements ISearchProvider {
}
private getRemoteSettingMatcher(scoredResults: IScoredResults, minScore: number, preferencesModel: ISettingsEditorModel): ISettingMatcher {
return (setting: ISetting) => {
const score = scoredResults[setting.key];
if (typeof score === 'number' && score >= minScore) {
const settingMatches = new SettingMatches(this._filter, setting, false, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches;
return { matches: settingMatches, score: scoredResults[setting.key] };
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, false, (filter, setting) => preferencesModel.findValueMatches(filter, setting)).matches;
return { matches: settingMatches, score: remoteSetting.score };
}
return null;
};
}
private prepareUrl(query: string, endpoint: IEndpointDetails, buildNumber: number): TPromise<string> {
private async prepareRequest(query: string): TPromise<{ url: string, body?: string }> {
query = escapeSpecialChars(query);
const boost = 10;
const userQuery = `(${query})^${boost}`;
......@@ -214,41 +273,73 @@ export class RemoteSearchProvider implements ISearchProvider {
query = query.replace(/\ +/g, '~ ') + '~';
const encodedQuery = encodeURIComponent(userQuery + ' || ' + query);
let url = `${endpoint.urlBase}?`;
let url = `${this.options.endpoint.urlBase}?`;
return this.installedExtensions.then(exts => {
if (endpoint.key) {
url += `${API_VERSION}`;
url += `&search=${encodedQuery}`;
const filters = exts.map(ext => {
const uuid = ext.identifier.uuid;
const versionString = ext.manifest.version
.split('.')
.map(versionPart => strings.pad(<any>versionPart, 10))
.join('');
return `(packageid eq '${uuid}' and startbuildno le '${versionString}' and endbuildno ge '${versionString}')`;
});
const buildNumber = this.environmentService.settingsSearchBuildId;
if (this.options.endpoint.key) {
url += `${API_VERSION}&${QUERY_TYPE}`;
}
if (buildNumber) {
filters.push(`(packageid eq 'core' and startbuildno le '${buildNumber}' and endbuildno ge '${buildNumber}')`);
url += `&$filter=${filters.join(' or ')}`;
}
} else {
url += `query=${encodedQuery}`;
if (this.options.usePost) {
const filters = this.options.newExtensionsOnly ?
[`diminish eq 'latest'`] :
await this.getVersionFilters(buildNumber);
if (buildNumber) {
url += `&build=${buildNumber}`;
}
const filterStr = encodeURIComponent(filters.join(' or '));
const body = JSON.stringify({
query: encodedQuery,
filters: filterStr
});
return {
url,
body
};
} else {
url += `query=${encodedQuery}`;
if (buildNumber) {
url += `&build=${buildNumber}`;
}
}
return TPromise.wrap({ url });
}
return url;
private getVersionFilters(buildNumber?: number): TPromise<string[]> {
return this.installedExtensions.then(exts => {
// 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(<any>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, ' ')
......@@ -257,6 +348,21 @@ function escapeSpecialChars(query: string): string {
.trim();
}
function remoteSettingToISetting(remoteSetting: IRemoteSetting): IExtensionSetting {
return {
description: remoteSetting.description.split('\n'),
descriptionRanges: null,
key: remoteSetting.key,
keyRange: null,
value: remoteSetting.defaultValue,
range: null,
valueRange: null,
overrides: [],
extensionName: remoteSetting.extensionName,
extensionPublisher: remoteSetting.extensionPublisher
};
}
class SettingMatches {
private readonly descriptionMatchingWords: Map<string, IRange[]> = new Map<string, IRange[]>();
......@@ -265,7 +371,7 @@ class SettingMatches {
public readonly matches: IRange[];
constructor(searchString: string, setting: ISetting, private requireFullQueryMatch: boolean, private valuesMatcher: (filter: string, setting: ISetting) => IRange[]) {
constructor(searchString: string, setting: ISetting, private requireFullQueryMatch: boolean, private searchDescription, private valuesMatcher: (filter: string, setting: ISetting) => IRange[]) {
this.matches = distinct(this._findMatchesInSetting(searchString, setting), (match) => `${match.startLineNumber}_${match.startColumn}_${match.endLineNumber}_${match.endColumn}_`);
}
......@@ -273,7 +379,7 @@ class SettingMatches {
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.requireFullQueryMatch, this.valuesMatcher);
const subSettingMatches = new SettingMatches(searchString, subSetting, this.requireFullQueryMatch, this.searchDescription, 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]);
......@@ -294,10 +400,12 @@ class SettingMatches {
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)));
if (this.searchDescription) {
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)));
}
}
}
......@@ -315,12 +423,14 @@ class SettingMatches {
}
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]));
if (this.searchDescription) {
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);
......@@ -331,7 +441,7 @@ class SettingMatches {
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);
valueRanges = this.valuesMatcher ? this.valuesMatcher(searchString, setting) : [];
}
return [...descriptionRanges, ...keyRanges, ...valueRanges];
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册