extension tips as quick pick list

This commit is contained in:
Johannes Rieken 2016-02-03 10:25:17 +01:00
parent ad09563752
commit 83f710bb8c
5 changed files with 296 additions and 4 deletions

View file

@ -53,3 +53,11 @@ export interface IExtensionsService {
uninstall(extension: IExtension): TPromise<void>;
getInstalled(includeDuplicateVersions?: boolean): TPromise<IExtension[]>;
}
export var IExtensionTipsService = createDecorator<IExtensionTipsService>('extensionTipsService');
export interface IExtensionTipsService {
serviceId: ServiceIdentifier<any>;
tips: IExtension[];
onDidChangeTips: Event<void>;
}

View file

@ -0,0 +1,180 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import URI from 'vs/base/common/uri';
import {values, forEach} from 'vs/base/common/collections';
import {isEmptyObject} from 'vs/base/common/types';
import {IDisposable, disposeAll} from 'vs/base/common/lifecycle';
import {TPromise as Promise} from 'vs/base/common/winjs.base';
import {match} from 'vs/base/common/glob';
import Event, {Emitter} from 'vs/base/common/event';
import {IExtensionsService, IGalleryService, IExtensionTipsService, IExtension} from 'vs/workbench/parts/extensions/common/extensions';
import {IModelService} from 'vs/editor/common/services/modelService';
import {EventType} from 'vs/editor/common/editorCommon';
interface ExtensionMap {
[id: string]: IExtension;
}
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.score - this.score;
if (result === 0) {
result = tip._touched - this._touched;
}
return result;
}
}
export class ExtensionTipsService implements IExtensionTipsService {
serviceId: any;
private _onDidChangeTips: Emitter<void> = new Emitter<void>();
private _tips: { [id: string]: ExtensionTip } = Object.create(null);
private _toDispose: IDisposable[] = [];
constructor(
@IExtensionsService private _extensionService: IExtensionsService,
@IGalleryService private _galleryService: IGalleryService,
@IModelService private _modelService: IModelService
) {
if (this._galleryService.isEnabled()) {
this._init();
}
}
dispose() {
this._toDispose = disposeAll(this._toDispose);
}
get onDidChangeTips(): Event<void> {
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);
}
private _init() {
this._galleryService.query().then(extensions => {
let map: ExtensionMap = Object.create(null);
for (let ext of extensions) {
map[`${ext.publisher}.${ext.name}`] = ext;
}
return this._extensionService.getInstalled().then(installed => {
for (let ext of installed) {
delete map[`${ext.publisher}.${ext.name}`];
}
return map;
});
}).then(extensions => {
// 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._toDispose.push({ dispose() { disposeAll(values(modelListener)) } });
this._toDispose.push(this._modelService.onModelAdded(model => {
const uri = model.getAssociatedResource();
this._suggestByResource(extensions, uri, ExtensionTipReasons.FileOpened);
modelListener[uri.toString()] = model.addListener2(EventType.ModelContentChanged2,
() => this._suggestByResource(extensions, uri, ExtensionTipReasons.FileEdited));
}));
this._toDispose.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()) {
this._suggestByResource(extensions, model.getAssociatedResource(), ExtensionTipReasons.FileOpened);
}
});
}
// --- suggest logic
private _suggestByResource(extensions: ExtensionMap, uri: URI, reason: ExtensionTipReasons): Promise<any> {
if (!uri) {
return;
}
let change = false;
forEach(ExtensionTipsService._extensionByPattern, entry => {
let extension = extensions[entry.key];
if (extension && match(entry.value, uri.fsPath)) {
let value = this._tips[entry.key];
if (!value) {
value = this._tips[entry.key] = new ExtensionTip(extension);
}
if (value.resource(uri, reason)) {
change = true;
}
}
});
if (change) {
this._onDidChangeTips.fire(undefined);
}
}
private static _extensionByPattern: { [pattern: string]: string } = {
'jrieken.vscode-omnisharp': '{**/*.cs,**/project.json,**/global.json,**/*.csproj,**/*.sln}',
'eg2.tslint': '**/*.ts',
'dbaeumer.vscode-eslint': '{**/*.js,**/*.es6}',
'mkaufman.HTMLHint': '{**/*.html,**/*.htm}',
'seanmcbreen.Spell': '**/*.md',
'ms-vscode.jscs': '{**/*.js,**/*.es6}',
'ms-vscode.wordcount': '**/*.md',
'Ionide.Ionide-fsharp': '{**/*.fsx,**/*.fsi,**/*.fs,**/*.ml,**/*.mli}'
}
}

View file

@ -83,6 +83,29 @@ export class ListOutdatedExtensionsAction extends Action {
}
}
export class ListSuggestedExtensionsAction extends Action {
static ID = 'workbench.extensions.action.listSuggestedExtensions';
static LABEL = nls.localize('showSuggestedExtensions', "Show Suggested Extensions");
constructor(
id: string,
label: string,
@IExtensionsService private extensionsService: IExtensionsService,
@IQuickOpenService private quickOpenService: IQuickOpenService
) {
super(id, label, null, true);
}
public run(): Promise {
return this.quickOpenService.show('ext tips ');
}
protected isEnabled(): boolean {
return true;
}
}
export class InstallAction extends Action {
constructor(

View file

@ -17,7 +17,7 @@ import { since } from 'vs/base/common/dates';
import { matchesContiguousSubString } from 'vs/base/common/filters';
import { QuickOpenHandler } from 'vs/workbench/browser/quickopen';
import { IHighlight } from 'vs/base/parts/quickopen/browser/quickOpenModel';
import { IExtensionsService, IGalleryService, IExtension } from 'vs/workbench/parts/extensions/common/extensions';
import { IExtensionsService, IGalleryService, IExtensionTipsService, IExtension } from 'vs/workbench/parts/extensions/common/extensions';
import { InstallAction, UninstallAction } from 'vs/workbench/parts/extensions/electron-browser/extensionsActions';
import { IMessageService } from 'vs/platform/message/common/message';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@ -531,4 +531,71 @@ export class OutdatedExtensionsHandler extends QuickOpenHandler {
getAutoFocus(searchValue: string): IAutoFocus {
return { autoFocusFirstEntry: true };
}
}
}
class SuggestedExtensionsModel implements IModel<IExtensionEntry> {
public dataSource = new DataSource();
public renderer: IRenderer<IExtensionEntry>;
public runner: IRunner<IExtensionEntry>;
public entries: IExtensionEntry[];
constructor(
private suggestedExtensions: IExtension[],
@IInstantiationService instantiationService: IInstantiationService
) {
this.renderer = instantiationService.createInstance(Renderer);
this.runner = instantiationService.createInstance(InstallRunner);
this.entries = [];
}
public set input(input: string) {
this.entries = this.suggestedExtensions
.map(extension => ({ extension, highlights: getHighlights(input, extension) }))
.filter(({ highlights }) => !!highlights)
.map(({ extension, highlights }: { extension: IExtension, highlights: IHighlights }) => {
return {
extension,
highlights,
state: ExtensionState.Uninstalled
};
});
}
}
export class SuggestedExtensionHandler extends QuickOpenHandler {
private model: SuggestedExtensionsModel;
constructor(
@IExtensionTipsService private extensionTipsService: IExtensionTipsService,
@IInstantiationService private instantiationService: IInstantiationService
) {
super();
}
getResults(input: string): TPromise<IModel<IExtensionEntry>> {
if (!this.model) {
this.model = this.instantiationService.createInstance(
SuggestedExtensionsModel,
this.extensionTipsService.tips);
}
this.model.input = input;
return TPromise.as(this.model);
}
onClose(canceled: boolean): void {
this.model = null;
}
getEmptyLabel(input: string): string {
return nls.localize('noSuggestedExtensions', "No suggested extensions");
}
getAutoFocus(searchValue: string): IAutoFocus {
return { autoFocusFirstEntry: true };
}
}

View file

@ -8,7 +8,7 @@ import errors = require('vs/base/common/errors');
import platform = require('vs/platform/platform');
import { Promise } from 'vs/base/common/winjs.base';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { IExtensionsService, IGalleryService } from 'vs/workbench/parts/extensions/common/extensions';
import { IExtensionsService, IGalleryService, IExtensionTipsService } from 'vs/workbench/parts/extensions/common/extensions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IMessageService } from 'vs/platform/message/common/message';
import Severity from 'vs/base/common/severity';
@ -16,7 +16,8 @@ import { IWorkspaceContextService } from 'vs/workbench/services/workspace/common
import { ReloadWindowAction } from 'vs/workbench/electron-browser/actions';
import wbaregistry = require('vs/workbench/common/actionRegistry');
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { ListExtensionsAction, InstallExtensionAction, ListOutdatedExtensionsAction } from './extensionsActions';
import { ListExtensionsAction, InstallExtensionAction, ListOutdatedExtensionsAction, ListSuggestedExtensionsAction } from './extensionsActions';
import { ExtensionTipsService } from './extensionTipsService';
import { IQuickOpenRegistry, Extensions, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen';
import { checkForLegacyExtensionNeeds } from './extensionsAssistant';
import {ipcRenderer as ipc} from 'electron';
@ -79,6 +80,19 @@ export class ExtensionsWorkbenchExtension implements IWorkbenchContribution {
nls.localize('outdatedExtensionsCommands', "Update Outdated Extensions")
)
);
// add extension tips services
instantiationService.addSingleton(IExtensionTipsService, this.instantiationService.createInstance(ExtensionTipsService));
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ListSuggestedExtensionsAction, ListSuggestedExtensionsAction.ID, ListSuggestedExtensionsAction.LABEL), extensionsCategory);
(<IQuickOpenRegistry>platform.Registry.as(Extensions.Quickopen)).registerQuickOpenHandler(
new QuickOpenHandlerDescriptor(
'vs/workbench/parts/extensions/electron-browser/extensionsQuickOpen',
'SuggestedExtensionHandler',
'ext tips ',
nls.localize('suggestedExtensionsCommands', "Install Suggested Extensions")
)
);
}
}