提交 eb257f59 编写于 作者: J Joao Moreno

improve extension recommendations

related to #3633
上级 7c803f78
...@@ -80,7 +80,7 @@ export class ListView<T> implements IScrollable, IDisposable { ...@@ -80,7 +80,7 @@ export class ListView<T> implements IScrollable, IDisposable {
this.items = []; this.items = [];
this.itemId = 0; this.itemId = 0;
this.rangeMap = new RangeMap(); this.rangeMap = new RangeMap();
this.renderers = toObject(renderers, r => r.templateId); this.renderers = toObject<IRenderer<T, any>, IRenderer<T, any>>(renderers, r => r.templateId);
this.cache = new RowCache(this.renderers); this.cache = new RowCache(this.renderers);
this.renderTop = 0; this.renderTop = 0;
......
...@@ -157,8 +157,8 @@ export function assign(destination: any, ...sources: any[]): any { ...@@ -157,8 +157,8 @@ export function assign(destination: any, ...sources: any[]): any {
return destination; return destination;
} }
export function toObject<T>(arr: T[], hash: (T) => string): { [key: string]: T } { export function toObject<T,R>(arr: T[], keyMap: (T) => string, valueMap: (T) => R = x => x): { [key: string]: R } {
return arr.reduce((o, d) => assign(o, { [hash(d)]: d }), Object.create(null)); return arr.reduce((o, d) => assign(o, { [keyMap(d)]: valueMap(d) }), Object.create(null));
} }
/** /**
......
...@@ -66,6 +66,5 @@ export var IExtensionTipsService = createDecorator<IExtensionTipsService>('exten ...@@ -66,6 +66,5 @@ export var IExtensionTipsService = createDecorator<IExtensionTipsService>('exten
export interface IExtensionTipsService { export interface IExtensionTipsService {
serviceId: ServiceIdentifier<any>; serviceId: ServiceIdentifier<any>;
tips: IExtension[]; getRecommendations(): TPromise<IExtension[]>;
onDidChangeTips: Event<IExtension[]>;
} }
\ No newline at end of file
...@@ -6,177 +6,82 @@ ...@@ -6,177 +6,82 @@
import 'vs/text!vs/workbench/parts/extensions/electron-browser/extensionTips.json'; import 'vs/text!vs/workbench/parts/extensions/electron-browser/extensionTips.json';
import URI from 'vs/base/common/uri'; import URI from 'vs/base/common/uri';
import {toObject} from 'vs/base/common/objects'; import {toObject} from 'vs/base/common/objects';
import {values, forEach} from 'vs/base/common/collections';
import {IDisposable, disposeAll} from 'vs/base/common/lifecycle'; import {IDisposable, disposeAll} from 'vs/base/common/lifecycle';
import {TPromise as Promise} from 'vs/base/common/winjs.base'; import {TPromise as Promise} from 'vs/base/common/winjs.base';
import {match} from 'vs/base/common/glob'; import {match} from 'vs/base/common/glob';
import Event, {Emitter} from 'vs/base/common/event'; import {IGalleryService, IExtensionTipsService, IExtension} from 'vs/workbench/parts/extensions/common/extensions';
import {IExtensionsService, IGalleryService, IExtensionTipsService, IExtension} from 'vs/workbench/parts/extensions/common/extensions';
import {IModelService} from 'vs/editor/common/services/modelService'; import {IModelService} from 'vs/editor/common/services/modelService';
import {EventType} from 'vs/editor/common/editorCommon'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
interface ExtensionMap { interface ExtensionRecommendations {
[id: string]: IExtension;
}
interface ExtensionData {
[id: string]: string; [id: string]: string;
} }
enum ExtensionTipReasons {
// FileExists = 1
FileOpened = 2,
FileEdited = 3
}
class ExtensionTip {
private resources: { [uri: string]: ExtensionTipReasons } = Object.create(null);
private touched = Date.now();
private _score = -1;
constructor(public extension: IExtension) {
//
}
resource(uri: URI, reason: ExtensionTipReasons): boolean {
if (reason !== this.resources[uri.toString()]) {
this.touched = Date.now();
this.resources[uri.toString()] = Math.max((this.resources[uri.toString()] || 0), reason);
this._score = - 1;
return true;
}
}
get score() {
if (this._score === -1) {
forEach(this.resources, entry => this._score += entry.value);
}
return this._score;
}
compareTo(tip: ExtensionTip): number {
if (this === tip) {
return 0;
}
let result = tip.touched - this.touched;
if (result === 0) {
result = tip.score - this.score;
}
return result;
}
}
export class ExtensionTipsService implements IExtensionTipsService { export class ExtensionTipsService implements IExtensionTipsService {
serviceId: any; serviceId: any;
private recommendations: { [id: string]: boolean; };
private _onDidChangeTips: Emitter<IExtension[]> = new Emitter<IExtension[]>();
private _tips: { [id: string]: ExtensionTip } = Object.create(null);
private disposables: IDisposable[] = []; private disposables: IDisposable[] = [];
private availableExtensions: Promise<ExtensionMap>; private availableRecommendations: Promise<ExtensionRecommendations>;
private extensionData: Promise<ExtensionData>;
constructor( constructor(
@IExtensionsService private extensionService: IExtensionsService,
@IGalleryService private galleryService: IGalleryService, @IGalleryService private galleryService: IGalleryService,
@IModelService private modelService: IModelService @IModelService private modelService: IModelService,
@IStorageService private storageService: IStorageService
) { ) {
this.init();
}
dispose() {
this.disposables = disposeAll(this.disposables);
}
get onDidChangeTips(): Event<IExtension[]> {
return this._onDidChangeTips.event;
}
get tips(): IExtension[] {
let tips = values(this._tips);
tips.sort((a, b) => a.compareTo(b));
return tips.map(tip => tip.extension);
}
// --- internals
private init():void {
if (!this.galleryService.isEnabled()) { if (!this.galleryService.isEnabled()) {
return; return;
} }
this.extensionData = new Promise((resolve, reject) => { this.recommendations = toObject(JSON.parse(storageService.get('extensionsAssistant/recommendations', StorageScope.GLOBAL, '[]')), id => id, () => true);
this.availableRecommendations = new Promise((resolve, reject) => {
require(['vs/text!vs/workbench/parts/extensions/electron-browser/extensionTips.json'], require(['vs/text!vs/workbench/parts/extensions/electron-browser/extensionTips.json'],
data => resolve(JSON.parse(data)), data => resolve(JSON.parse(data)),
reject); () => ({}));
}); });
this.availableExtensions = this.getAvailableExtensions();
// we listen for editor models being added and changed
// when a model is added it gives 2 points, a change gives 3 points
// such that files you type have bigger impact on the suggest
// order than those you only look at
const modelListener: { [uri: string]: IDisposable } = Object.create(null);
this.disposables.push({ dispose() { disposeAll(values(modelListener)); } });
this.disposables.push(this.modelService.onModelAdded(model => { this.disposables.push(this.modelService.onModelAdded(model => {
const uri = model.getAssociatedResource(); this.suggest(model.getAssociatedResource());
this.suggestByResource(uri, ExtensionTipReasons.FileOpened);
modelListener[uri.toString()] = model.addListener2(EventType.ModelContentChanged2,
() => this.suggestByResource(uri, ExtensionTipReasons.FileEdited));
}));
this.disposables.push(this.modelService.onModelRemoved(model => {
const subscription = modelListener[model.getAssociatedResource().toString()];
if (subscription) {
subscription.dispose();
delete modelListener[model.getAssociatedResource().toString()];
}
})); }));
for (let model of this.modelService.getModels()) { for (let model of this.modelService.getModels()) {
this.suggestByResource(model.getAssociatedResource(), ExtensionTipReasons.FileOpened); this.suggest(model.getAssociatedResource());
} }
} }
private getAvailableExtensions(): Promise<ExtensionMap> { getRecommendations(): Promise<IExtension[]> {
return this.galleryService.query() return this.galleryService.query()
.then(null, () => []) .then(null, () => [])
.then(extensions => toObject(extensions, ext => `${ext.publisher}.${ext.name}`)); .then(available => toObject(available, ext => `${ext.publisher}.${ext.name}`))
.then(available => {
return Object.keys(this.recommendations)
.map(id => available[id])
.filter(i => !!i);
});
} }
// --- suggest logic private suggest(uri: URI): Promise<any> {
private suggestByResource(uri: URI, reason: ExtensionTipReasons): Promise<any> {
if (!uri) { if (!uri) {
return; return;
} }
Promise.join<any>([this.availableExtensions, this.extensionData]).then(all => { this.availableRecommendations.done(availableRecommendations => {
let extensions = <ExtensionMap>all[0]; const ids = Object.keys(availableRecommendations);
let data = <ExtensionData>all[1]; const recommendations = ids
.filter(id => match(availableRecommendations[id], uri.fsPath));
let change = false; recommendations.forEach(r => this.recommendations[r] = true);
forEach(data, entry => {
let extension = extensions[entry.key]; this.storageService.store(
if (extension && match(entry.value, uri.fsPath)) { 'extensionsAssistant/recommendations',
let value = this._tips[entry.key]; JSON.stringify(Object.keys(this.recommendations)),
if (!value) { StorageScope.GLOBAL
value = this._tips[entry.key] = new ExtensionTip(extension); );
}
if (value.resource(uri, reason)) {
change = true;
}
}
});
if (change) {
this._onDidChangeTips.fire(this.tips);
}
}, () => {
// ignore
}); });
} }
dispose() {
this.disposables = disposeAll(this.disposables);
}
} }
...@@ -98,7 +98,7 @@ export class ListSuggestedExtensionsAction extends Action { ...@@ -98,7 +98,7 @@ export class ListSuggestedExtensionsAction extends Action {
} }
public run(): Promise { public run(): Promise {
return this.quickOpenService.show('ext tips '); return this.quickOpenService.show('ext recommend ');
} }
protected isEnabled(): boolean { protected isEnabled(): boolean {
......
...@@ -579,14 +579,15 @@ class SuggestedExtensionsModel implements IModel<IExtensionEntry> { ...@@ -579,14 +579,15 @@ class SuggestedExtensionsModel implements IModel<IExtensionEntry> {
highlights, highlights,
state: ExtensionState.Uninstalled state: ExtensionState.Uninstalled
}; };
}); })
.sort(extensionEntryCompare);
} }
} }
export class SuggestedExtensionHandler extends QuickOpenHandler { export class SuggestedExtensionHandler extends QuickOpenHandler {
private model: SuggestedExtensionsModel; private modelPromise: TPromise<SuggestedExtensionsModel>;
constructor( constructor(
@IExtensionTipsService private extensionTipsService: IExtensionTipsService, @IExtensionTipsService private extensionTipsService: IExtensionTipsService,
...@@ -598,20 +599,20 @@ export class SuggestedExtensionHandler extends QuickOpenHandler { ...@@ -598,20 +599,20 @@ export class SuggestedExtensionHandler extends QuickOpenHandler {
} }
getResults(input: string): TPromise<IModel<IExtensionEntry>> { getResults(input: string): TPromise<IModel<IExtensionEntry>> {
return this.extensionsService.getInstalled().then(localExtensions => { if (!this.modelPromise) {
const model = this.instantiationService.createInstance( this.telemetryService.publicLog('extensionRecommendations:open');
SuggestedExtensionsModel, this.modelPromise = TPromise.join<any>([this.extensionTipsService.getRecommendations(), this.extensionsService.getInstalled()])
this.extensionTipsService.tips, .then(result => this.instantiationService.createInstance(SuggestedExtensionsModel, result[0], result[1]));
localExtensions }
);
return this.modelPromise.then(model => {
model.input = input; model.input = input;
return model; return model;
}); });
} }
onClose(canceled: boolean): void { onClose(canceled: boolean): void {
this.model = null; this.modelPromise = null;
} }
getEmptyLabel(input: string): string { getEmptyLabel(input: string): string {
......
...@@ -91,7 +91,7 @@ export class ExtensionsWorkbenchExtension implements IWorkbenchContribution { ...@@ -91,7 +91,7 @@ export class ExtensionsWorkbenchExtension implements IWorkbenchContribution {
new QuickOpenHandlerDescriptor( new QuickOpenHandlerDescriptor(
'vs/workbench/parts/extensions/electron-browser/extensionsQuickOpen', 'vs/workbench/parts/extensions/electron-browser/extensionsQuickOpen',
'SuggestedExtensionHandler', 'SuggestedExtensionHandler',
'ext tips ', 'ext recommend ',
nls.localize('suggestedExtensionsCommands', "Show Extension Recommendations") nls.localize('suggestedExtensionsCommands', "Show Extension Recommendations")
) )
); );
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册