diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 5230460854e..201cfc7378f 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -57,6 +57,10 @@ { "fileMatch": "%APP_SETTINGS_HOME%/snippets/*.json", "url": "vscode://schemas/snippets" + }, + { + "fileMatch": "/.vscode/extensions.json", + "url": "vscode://schemas/extensions" } ] } diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 4dd3ebf34d0..3d636c1e9d4 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -202,6 +202,7 @@ export const IExtensionTipsService = createDecorator('ext export interface IExtensionTipsService { _serviceBrand: any; getRecommendations(): string[]; + getWorkspaceRecommendations(): string[]; } export const ExtensionsLabel = nls.localize('extensions', "Extensions"); diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts index c349c71e48e..02e4dd79fad 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts @@ -11,6 +11,7 @@ import {TPromise as Promise} from 'vs/base/common/winjs.base'; import {Action} from 'vs/base/common/actions'; import {match} from 'vs/base/common/glob'; import {IExtensionManagementService, IExtensionGalleryService, IExtensionTipsService} from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionsConfiguration, EXTENSIONS_CONFIGURAION_NAME } from './extensions'; import {IModelService} from 'vs/editor/common/services/modelService'; import {IStorageService, StorageScope} from 'vs/platform/storage/common/storage'; import product from 'vs/platform/product'; @@ -18,6 +19,7 @@ import { IMessageService, CloseAction } from 'vs/platform/message/common/messag import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ShowRecommendedExtensionsAction } from './extensionsActions'; import Severity from 'vs/base/common/severity'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class ExtensionTipsService implements IExtensionTipsService { @@ -35,7 +37,8 @@ export class ExtensionTipsService implements IExtensionTipsService { @IStorageService private storageService: IStorageService, @IMessageService private messageService: IMessageService, @IExtensionManagementService private extensionsService: IExtensionManagementService, - @IInstantiationService private instantiationService: IInstantiationService + @IInstantiationService private instantiationService: IInstantiationService, + @IConfigurationService private configurationService: IConfigurationService ) { if (!this._galleryService.isEnabled()) { return; @@ -72,6 +75,11 @@ export class ExtensionTipsService implements IExtensionTipsService { this._modelService.getModels().forEach(model => this._suggest(model.uri)); } + getWorkspaceRecommendations(): string[] { + let configuration = this.configurationService.getConfiguration(EXTENSIONS_CONFIGURAION_NAME); + return configuration.recommendations ? configuration.recommendations : []; + } + getRecommendations(): string[] { return Object.keys(this._recommendations); } diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts b/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts index b1bcce88bb3..d43fae7e0e7 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts @@ -21,13 +21,15 @@ import { IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/co import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { VIEWLET_ID, IExtensionsWorkbenchService } from './extensions'; import { ExtensionsWorkbenchService } from './extensionsWorkbenchService'; -import { OpenExtensionsViewletAction, InstallExtensionsAction, ShowOutdatedExtensionsAction, ShowRecommendedExtensionsAction, ShowPopularExtensionsAction, ShowInstalledExtensionsAction, UpdateAllAction, OpenExtensionsFolderAction } from './extensionsActions'; +import { OpenExtensionsViewletAction, InstallExtensionsAction, ShowOutdatedExtensionsAction, ShowRecommendedExtensionsAction, ShowWorkspaceRecommendedExtensionsAction, ShowPopularExtensionsAction, ShowInstalledExtensionsAction, UpdateAllAction, OpenExtensionsFolderAction, ConfigureWorkspaceRecommendationsAction } from './extensionsActions'; import { ExtensionsInput } from './extensionsInput'; import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor } from 'vs/workbench/browser/viewlet'; import { ExtensionEditor } from './extensionEditor'; import { StatusUpdater } from './extensionsViewlet'; import { IQuickOpenRegistry, Extensions, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; +import jsonContributionRegistry = require('vs/platform/jsonschemas/common/jsonContributionRegistry'); +import { Schema, SchemaId } from 'vs/workbench/parts/extensions/electron-browser/extensionsFileTemplate'; // Singletons registerSingleton(IExtensionGalleryService, ExtensionGalleryService); @@ -104,6 +106,9 @@ actionRegistry.registerWorkbenchAction(listOutdatedActionDescriptor, 'Extensions const recommendationsActionDescriptor = new SyncActionDescriptor(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, ShowRecommendedExtensionsAction.LABEL); actionRegistry.registerWorkbenchAction(recommendationsActionDescriptor, 'Extensions: Show Recommended Extensions', ExtensionsLabel); +const workspaceRecommendationsActionDescriptor = new SyncActionDescriptor(ShowWorkspaceRecommendedExtensionsAction, ShowWorkspaceRecommendedExtensionsAction.ID, ShowWorkspaceRecommendedExtensionsAction.LABEL); +actionRegistry.registerWorkbenchAction(workspaceRecommendationsActionDescriptor, 'Extensions: Show Workspace Recommended Extensions', ExtensionsLabel); + const popularActionDescriptor = new SyncActionDescriptor(ShowPopularExtensionsAction, ShowPopularExtensionsAction.ID, ShowPopularExtensionsAction.LABEL); actionRegistry.registerWorkbenchAction(popularActionDescriptor, 'Extensions: Show Popular Extensions', ExtensionsLabel); @@ -116,6 +121,9 @@ actionRegistry.registerWorkbenchAction(updateAllActionDescriptor, 'Extensions: U const openExtensionsFolderActionDescriptor = new SyncActionDescriptor(OpenExtensionsFolderAction, OpenExtensionsFolderAction.ID, OpenExtensionsFolderAction.LABEL); actionRegistry.registerWorkbenchAction(openExtensionsFolderActionDescriptor, 'Extensions: Open Extensions Folder', ExtensionsLabel); +const openExtensionsFileActionDescriptor = new SyncActionDescriptor(ConfigureWorkspaceRecommendationsAction, ConfigureWorkspaceRecommendationsAction.ID, ConfigureWorkspaceRecommendationsAction.LABEL); +actionRegistry.registerWorkbenchAction(openExtensionsFileActionDescriptor, 'Extensions: Open Extensions File', ExtensionsLabel); + Registry.as(ConfigurationExtensions.Configuration) .registerConfiguration({ id: 'extensions', @@ -130,3 +138,6 @@ Registry.as(ConfigurationExtensions.Configuration) } } }); + +const jsonRegistry = Registry.as(jsonContributionRegistry.Extensions.JSONContribution); +jsonRegistry.registerSchema(SchemaId, Schema); \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensions.ts b/src/vs/workbench/parts/extensions/electron-browser/extensions.ts index 6966da5edce..8408f2b4c14 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensions.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensions.ts @@ -59,8 +59,12 @@ export interface IExtensionsWorkbenchService { canInstall(extension: IExtension): boolean; install(extension: IExtension): TPromise; uninstall(extension: IExtension): TPromise; + openExtensionsFile(sideBySide?: boolean): TPromise; } +export const EXTENSIONS_CONFIGURAION_NAME = 'extensions'; + export interface IExtensionsConfiguration { autoUpdate: boolean; + recommendations: string[]; } \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts index 1258c9ba391..dfcd2cf9a43 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts @@ -458,6 +458,33 @@ export class ShowRecommendedExtensionsAction extends Action { } } +export class ShowWorkspaceRecommendedExtensionsAction extends Action { + + static ID = 'workbench.extensions.action.showWorkspaceRecommendedExtensions'; + static LABEL = localize('showWorkspaceRecommendedExtensions', "Show Workspace Recommended Extensions"); + + constructor( + id: string, + label: string, + @IViewletService private viewletService: IViewletService + ) { + super(id, label, null, true); + } + + run(): TPromise { + return this.viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + viewlet.search('@recommended:workspace'); + viewlet.focus(); + }); + } + + protected isEnabled(): boolean { + return true; + } +} + export class ChangeSortAction extends Action { private query: Query; @@ -525,4 +552,17 @@ export class OpenExtensionsFolderAction extends Action { protected isEnabled(): boolean { return true; } +} + +export class ConfigureWorkspaceRecommendationsAction extends Action { + static ID = 'workbench.extensions.action.configureWorkspaceRecommendations'; + static LABEL = localize('configureWorkspaceRecommendations', "Configure Worksapce Recommendations"); + + constructor(id: string, label: string, @IExtensionsWorkbenchService private extensionsService: IExtensionsWorkbenchService) { + super(id, label, null, true); + } + + public run(event: any): TPromise { + return this.extensionsService.openExtensionsFile(); + } } \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsFileTemplate.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsFileTemplate.ts new file mode 100644 index 00000000000..5fc1c0b9018 --- /dev/null +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsFileTemplate.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; + +export const SchemaId = 'vscode://schemas/extensions'; +export const Schema: IJSONSchema = { + id: SchemaId, + type: 'object', + title: localize('app.extensions.json.title', "Extensions"), + properties: { + recommendations: { + type: 'array', + description: localize('app.extensions.json.recommendations', "List of extension recommendations."), + items: { + 'type': 'string', + } + } + } +}; + +export const Content: string = [ + '{', + '\t// See https://go.microsoft.com/fwlink/?LinkId=733558', + '\t// for the documentation about the extensions.json format', + '\t"recommendations": [', + '\t\t', + '\t]', + '}' +].join('\n'); \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts index 68faff42e30..4a3d5c0ce6f 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts @@ -30,7 +30,7 @@ import { PagedList } from 'vs/base/browser/ui/list/listPaging'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Delegate, Renderer } from './extensionsList'; import { IExtensionsWorkbenchService, IExtension, IExtensionsViewlet, VIEWLET_ID, ExtensionState } from './extensions'; -import { ShowRecommendedExtensionsAction, ShowPopularExtensionsAction, ShowInstalledExtensionsAction, ShowOutdatedExtensionsAction, ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction } from './extensionsActions'; +import { ShowRecommendedExtensionsAction, ShowWorkspaceRecommendedExtensionsAction, ShowPopularExtensionsAction, ShowInstalledExtensionsAction, ShowOutdatedExtensionsAction, ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction } from './extensionsActions'; import { IExtensionManagementService, IExtensionGalleryService, IExtensionTipsService, SortBy, SortOrder, IQueryOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionsInput } from './extensionsInput'; import { Query } from '../common/extensionQuery'; @@ -168,6 +168,7 @@ export class ExtensionsViewlet extends Viewlet implements IExtensionsViewlet { this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL), this.instantiationService.createInstance(ShowOutdatedExtensionsAction, ShowOutdatedExtensionsAction.ID, ShowOutdatedExtensionsAction.LABEL), this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, ShowRecommendedExtensionsAction.LABEL), + this.instantiationService.createInstance(ShowWorkspaceRecommendedExtensionsAction, ShowWorkspaceRecommendedExtensionsAction.ID, ShowWorkspaceRecommendedExtensionsAction.LABEL), this.instantiationService.createInstance(ShowPopularExtensionsAction, ShowPopularExtensionsAction.ID, ShowPopularExtensionsAction.LABEL), new Separator(), this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.install', localize('sort by installs', "Sort By: Install Count"), this.onSearchChange, 'installs', undefined), @@ -230,6 +231,10 @@ export class ExtensionsViewlet extends Viewlet implements IExtensionsViewlet { case 'desc': options = assign(options, { sortOrder: SortOrder.Descending }); break; } + if (/@recommended:workspace/i.test(query.value)) { + return this.queryWorkspaceRecommendations(); + } + if (/@recommended/i.test(query.value)) { const value = query.value.replace(/@recommended/g, '').trim().toLowerCase(); @@ -257,6 +262,15 @@ export class ExtensionsViewlet extends Viewlet implements IExtensionsViewlet { .then(result => new PagedModel(result)); } + private queryWorkspaceRecommendations(): TPromise> { + let names = this.tipsService.getWorkspaceRecommendations(); + if (!names.length) { + return TPromise.as(new PagedModel([])); + } + return this.extensionsWorkbenchService.queryGallery({ names, pageSize: names.length }) + .then(result => new PagedModel(result)); + } + private openExtension(extension: IExtension): void { this.editorService.openEditor(this.instantiationService.createInstance(ExtensionsInput, extension)) .done(null, err => this.onError(err)); diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsWorkbenchService.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsWorkbenchService.ts index 7cab830d8c2..b1696252a3f 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsWorkbenchService.ts @@ -6,6 +6,8 @@ 'use strict'; import 'vs/css!./media/extensionsViewlet'; +import { localize } from 'vs/nls'; +import paths = require('vs/base/common/paths'); import Event, { Emitter } from 'vs/base/common/event'; import { index } from 'vs/base/common/arrays'; import { assign } from 'vs/base/common/objects'; @@ -21,15 +23,18 @@ import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IMessageService } from 'vs/platform/message/common/message'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import Severity from 'vs/base/common/severity'; import * as semver from 'semver'; import * as path from 'path'; import URI from 'vs/base/common/uri'; import { readFile } from 'vs/base/node/pfs'; import { asText } from 'vs/base/node/request'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, IExtensionsConfiguration } from './extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, IExtensionsConfiguration, EXTENSIONS_CONFIGURAION_NAME } from './extensions'; import { UpdateAllAction } from './extensionsActions'; - +import { IFileService } from 'vs/platform/files/common/files'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { Content } from 'vs/workbench/parts/extensions/electron-browser/extensionsFileTemplate'; interface IExtensionStateProvider { (extension: Extension): ExtensionState; @@ -252,6 +257,9 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { constructor( @IInstantiationService private instantiationService: IInstantiationService, + @IWorkspaceContextService private contextService: IWorkspaceContextService, + @IFileService private fileService: IFileService, + @IWorkbenchEditorService private editorService: IWorkbenchEditorService, @IExtensionManagementService private extensionService: IExtensionManagementService, @IExtensionGalleryService private galleryService: IExtensionGalleryService, @IConfigurationService private configurationService: IConfigurationService, @@ -338,7 +346,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { } return this.queryGallery({ ids, pageSize: ids.length }).then(() => { - const config = this.configurationService.getConfiguration('extensions'); + const config = this.configurationService.getConfiguration(EXTENSIONS_CONFIGURAION_NAME); if (!config.autoUpdate) { return; @@ -502,6 +510,36 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { this.messageService.show(Severity.Error, err); } + openExtensionsFile(sideBySide?: boolean): TPromise { + if (!this.contextService.getWorkspace()) { + this.messageService.show(Severity.Info, localize('ConfigureWorkspaceRecommendations.noWorkspace', 'Recommendations are only available on a workspace folder.')); + return TPromise.as(undefined); + } + + return this.getOrCreateExtensionsFile().then(value => { + return this.editorService.openEditor({ + resource: value.extensionsFileResource, + options: { + forceOpen: true, + pinned: value.created + }, + }, sideBySide); + }, (error) => { + throw new Error(localize('OpenExtensionsFile.failed', "Unable to create 'extensions.json' file inside the '.vscode' folder ({0}).", error)); + }); + } + + private getOrCreateExtensionsFile(): TPromise<{ created: boolean, extensionsFileResource: URI }> { + let extensionsFileResource = URI.file(paths.join(this.contextService.getWorkspace().resource.fsPath, '/.vscode/' + EXTENSIONS_CONFIGURAION_NAME + '.json')); + return this.fileService.resolveContent(extensionsFileResource).then(content => { + return { created: false, extensionsFileResource }; + }, err => { + return this.fileService.updateContent(extensionsFileResource, Content).then(() => { + return { created: true, extensionsFileResource }; + }); + }); + } + dispose(): void { this.disposables = dispose(this.disposables); }