diff --git a/extensions/azurecore/package.json b/extensions/azurecore/package.json index d70b75d5cd..45f5f47bd7 100644 --- a/extensions/azurecore/package.json +++ b/extensions/azurecore/package.json @@ -19,6 +19,13 @@ "url": "https://github.com/Microsoft/azuredatastudio.git" }, "contributes": { + "resourceViewResources": [ + { + "id": "azure-resources", + "name": "Azure", + "icon": "./resources/azure.svg" + } + ], "configuration": [ { "type": "object", diff --git a/src/sql/platform/resourceViewer/common/resourceViewerRegistry.ts b/src/sql/platform/resourceViewer/common/resourceViewerRegistry.ts new file mode 100644 index 0000000000..8085371251 --- /dev/null +++ b/src/sql/platform/resourceViewer/common/resourceViewerRegistry.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Registry } from 'vs/platform/registry/common/platform'; + +export const Extensions = { + ResourceViewerExtension: 'resourceViewer.resources' +}; + +export interface ResourceType { + readonly id: string; + readonly icon: string; + readonly name: string; +} + +export interface ResourceViewerResourcesRegistry { + registerResource(resource: ResourceType): void; + readonly allResources: ReadonlyArray; + readonly onDidRegisterResource: Event; +} + +const resourceViewerResourceRegistery = new class implements ResourceViewerResourcesRegistry { + + private readonly resources: ResourceType[] = []; + private readonly _onDidRegisterResource = new Emitter(); + public readonly onDidRegisterResource = this._onDidRegisterResource.event; + + public registerResource(resource: ResourceType): void { + this.resources.push(Object.assign({}, resource)); + this._onDidRegisterResource.fire(); + } + + get allResources(): ReadonlyArray { + return this.resources; + } +}; + +Registry.add(Extensions.ResourceViewerExtension, resourceViewerResourceRegistery); diff --git a/src/sql/workbench/contrib/resourceViewer/browser/media/resourceViewerView.css b/src/sql/workbench/contrib/resourceViewer/browser/media/resourceViewerView.css new file mode 100644 index 0000000000..29c17380e7 --- /dev/null +++ b/src/sql/workbench/contrib/resourceViewer/browser/media/resourceViewerView.css @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-pane-view .pane .pane-body .resource-view { + height: 100%; + width: 100%; +} diff --git a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewer.contribution.ts b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewer.contribution.ts index 467e8cfb04..e63a4df169 100644 --- a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewer.contribution.ts +++ b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewer.contribution.ts @@ -12,6 +12,16 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { isString } from 'vs/base/common/types'; +import { IViewContainersRegistry, ViewContainerLocation, Extensions as ViewExtensions, IViewsRegistry } from 'vs/workbench/common/views'; +import { RESOURCE_VIEWER_VIEW_CONTAINER_ID, RESOURCE_VIEWER_VIEW_ID } from 'sql/workbench/contrib/resourceViewer/common/resourceViewer'; +import { localize } from 'vs/nls'; +import { Codicon } from 'vs/base/common/codicons'; +import { ResourceViewerViewlet } from 'sql/workbench/contrib/resourceViewer/browser/resourceViewerViewlet'; +import { ResourceViewerView } from 'sql/workbench/contrib/resourceViewer/browser/resourceViewerView'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ResourceViewResourcesExtensionHandler } from 'sql/workbench/contrib/resourceViewer/common/resourceViewerViewExtensionPoint'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; CommandsRegistry.registerCommand({ id: 'resourceViewer.openResourceViewer', @@ -36,3 +46,42 @@ const resourceViewerDescriptor = EditorDescriptor.create( Registry.as(EditorExtensions.Editors) .registerEditor(resourceViewerDescriptor, [new SyncDescriptor(ResourceViewerInput)]); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ResourceViewResourcesExtensionHandler, LifecyclePhase.Ready); + +class ResourceViewerContributor implements IWorkbenchContribution { + constructor( + @IExtensionService private readonly extensionService: IExtensionService + ) { + void this.checkForArc(); + } + + private async checkForArc(): Promise { + if (await this.extensionService.getExtension('Microsoft.arc')) { + registerResourceViewerContainer(); + } else { + const disposable = this.extensionService.onDidChangeExtensions(async () => { + if (await this.extensionService.getExtension('Microsoft.arc')) { + registerResourceViewerContainer(); + disposable.dispose(); + } + }); + } + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ResourceViewerContributor, LifecyclePhase.Ready); + +function registerResourceViewerContainer() { + const viewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ + id: RESOURCE_VIEWER_VIEW_CONTAINER_ID, + name: localize('resourceViewer', "Resource Viewer"), + ctorDescriptor: new SyncDescriptor(ResourceViewerViewlet), + icon: Codicon.database.classNames, + alwaysUseContainerInfo: true + }, ViewContainerLocation.Sidebar); + // registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenDebugViewletAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_D }), 'View: Show Run and Debug', nls.localize('view', "View")); + + // Register default debug views + const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); + viewsRegistry.registerViews([{ id: RESOURCE_VIEWER_VIEW_ID, name: localize('resourceViewer', "Resource Viewer"), containerIcon: Codicon.database.classNames, ctorDescriptor: new SyncDescriptor(ResourceViewerView), canToggleVisibility: false, canMoveView: false }], viewContainer); +} diff --git a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerView.ts b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerView.ts new file mode 100644 index 0000000000..21c4ed2915 --- /dev/null +++ b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerView.ts @@ -0,0 +1,162 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/resourceViewerView'; +import { ResourceType, ResourceViewerResourcesRegistry, Extensions } from 'sql/platform/resourceViewer/common/resourceViewerRegistry'; +import { append, $ } from 'vs/base/browser/dom'; +import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { IDataSource, ITreeMouseEvent, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { WorkbenchDataTree } from 'vs/platform/list/browser/listService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IViewDescriptorService } from 'vs/workbench/common/views'; + +type TreeElement = ResourceType; + +export class ResourceViewerView extends ViewPane { + private listContainer!: HTMLElement; + private tree!: WorkbenchDataTree; + private model!: TreeModel; + + constructor( + options: IViewPaneOptions, + @IKeybindingService keybindingService: IKeybindingService, + @IThemeService themeService: IThemeService, + @IContextMenuService contextMenuService: IContextMenuService, + @ICommandService private readonly commandService: ICommandService, + @IInstantiationService instantiationService: IInstantiationService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IConfigurationService configurationService: IConfigurationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IOpenerService openerService: IOpenerService, + @ITelemetryService telemetryService: ITelemetryService + ) { + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + } + + protected renderBody(container: HTMLElement): void { + super.renderBody(container); + + this.listContainer = append(container, $('.resource-view')); + + const renderers: ITreeRenderer[] = [new ResourceRenderer()]; + + this.model = new TreeModel(); + + this.tree = this.instantiationService.createInstance( + WorkbenchDataTree, + 'Resource View', + this.listContainer, + new ListDelegate(), + renderers, + new DataSource(), + { + identityProvider: new IdentityProvider(), + horizontalScrolling: false, + setRowLineHeight: false, + transformOptimization: false, + accessibilityProvider: new ListAccessibilityProvider() + }) as WorkbenchDataTree; + + this.tree.setInput(this.model); + + this._register(Registry.as(Extensions.ResourceViewerExtension).onDidRegisterResource(() => this.tree.updateChildren(this.model))); + this._register(this.tree.onMouseDblClick(this.onDoubleClick, this)); + } + + private onDoubleClick(event: ITreeMouseEvent) { + if (event.element) { + this.commandService.executeCommand('resourceViewer.openResourceViewer', event.element.id); + } + } + + protected layoutBody(height: number, width: number): void { + super.layoutBody(height, width); + this.tree.layout(height, width); + } +} + +interface ResourceTypeTemplate { + readonly icon: HTMLElement; + readonly name: HTMLElement; +} + +class ResourceRenderer implements ITreeRenderer { + public static TEMPLATEID = 'resourceType'; + public readonly templateId = ResourceRenderer.TEMPLATEID; + + renderTemplate(parent: HTMLElement): ResourceTypeTemplate { + const container = append(parent, $('span')); + const icon = append(container, $('.icon')); + const name = append(container, $('.name')); + return { name, icon }; + } + + renderElement(element: ITreeNode, index: number, templateData: ResourceTypeTemplate, height: number): void { + templateData.name.innerText = element.element.name; + } + + disposeTemplate(templateData: ResourceTypeTemplate): void { + + } + +} + +class ListDelegate implements IListVirtualDelegate { + getHeight(): number { + return 22; + } + + getTemplateId(element: TreeElement): string { + return ResourceRenderer.TEMPLATEID; + } +} + +class IdentityProvider implements IIdentityProvider { + getId(element: TreeElement): string { + return element.id; + } +} + +class TreeModel { + private readonly registry = Registry.as(Extensions.ResourceViewerExtension); + + getChildren(): ResourceType[] { + return this.registry.allResources.slice(); + } +} + +class ListAccessibilityProvider implements IListAccessibilityProvider { + getAriaLabel(element: TreeElement): string { + return element.name; + } + + getWidgetAriaLabel(): string { + return 'Resource Viewer Tree'; + } +} + +class DataSource implements IDataSource { + hasChildren(element: TreeModel | TreeElement): boolean { + return element instanceof TreeModel; + } + + getChildren(element: TreeModel | TreeElement): Iterable { + if (element instanceof TreeModel) { + return element.getChildren(); + } + return []; + } +} diff --git a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerViewlet.ts b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerViewlet.ts new file mode 100644 index 0000000000..f8bc26ad4f --- /dev/null +++ b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerViewlet.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RESOURCE_VIEWER_VIEW_CONTAINER_ID } from 'sql/workbench/contrib/resourceViewer/common/resourceViewer'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; + +export class ResourceViewerViewlet extends ViewPaneContainer { + constructor( + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + @ITelemetryService telemetryService: ITelemetryService, + @IInstantiationService instantiationService: IInstantiationService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IStorageService storageService: IStorageService, + @IThemeService themeService: IThemeService, + @IContextMenuService contextMenuService: IContextMenuService, + @IExtensionService extensionService: IExtensionService, + @IConfigurationService configurationService: IConfigurationService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService + ) { + super(RESOURCE_VIEWER_VIEW_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); + } +} diff --git a/src/sql/workbench/contrib/resourceViewer/common/resourceViewer.ts b/src/sql/workbench/contrib/resourceViewer/common/resourceViewer.ts new file mode 100644 index 0000000000..e62d375cce --- /dev/null +++ b/src/sql/workbench/contrib/resourceViewer/common/resourceViewer.ts @@ -0,0 +1,7 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const RESOURCE_VIEWER_VIEW_CONTAINER_ID = 'workbench.viewContainer.resourceViewer'; +export const RESOURCE_VIEWER_VIEW_ID = 'workbench.view.resourceViewer'; diff --git a/src/sql/workbench/contrib/resourceViewer/common/resourceViewerViewExtensionPoint.ts b/src/sql/workbench/contrib/resourceViewer/common/resourceViewerViewExtensionPoint.ts new file mode 100644 index 0000000000..4205f992da --- /dev/null +++ b/src/sql/workbench/contrib/resourceViewer/common/resourceViewerViewExtensionPoint.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IExtensionPoint, ExtensionsRegistry, ExtensionMessageCollector } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { ResourceViewerResourcesRegistry, Extensions } from 'sql/platform/resourceViewer/common/resourceViewerRegistry'; + +interface IUserFriendlyViewDescriptor { + id: string; + name: string; + icon: string; +} + +const viewDescriptor: IJSONSchema = { + type: 'object', + properties: { + id: { + description: localize('extension.contributes.resourceView.resource.id', "Identifier of the resource."), + type: 'string' + }, + name: { + description: localize('extension.contributes.resourceView.resource.name', "The human-readable name of the view. Will be shown"), + type: 'string' + }, + icon: { + description: localize('extension.contributes.resourceView.resource.icon', "Path to the resource icon."), + type: 'string' + }, + } +}; + +const resourceViewResourcesContribution: IJSONSchema = { + description: localize('extension.contributes.resourceViewResources', "Contributes resource to the resource view"), + type: 'array', + items: viewDescriptor, + default: [] +}; + +const resourceViewExtensionPoint: IExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'resourceViewResources', jsonSchema: resourceViewResourcesContribution }); + +export class ResourceViewResourcesExtensionHandler implements IWorkbenchContribution { + + constructor() { + this.handleAndRegisterCustomViews(); + } + + private handleAndRegisterCustomViews() { + const resourceRegistry = Registry.as(Extensions.ResourceViewerExtension); + resourceViewExtensionPoint.setHandler(extensions => { + for (let extension of extensions) { + const { value, collector } = extension; + + for (const descriptor of value) { + if (!this.isValidResource(descriptor, collector)) { + return; + } + + resourceRegistry.registerResource(descriptor); + } + } + }); + } + + private isValidResource(descriptor: IUserFriendlyViewDescriptor, collector: ExtensionMessageCollector): boolean { + if (typeof descriptor.id !== 'string') { + collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'id')); + return false; + } + if (typeof descriptor.name !== 'string') { + collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'name')); + return false; + } + if (typeof descriptor.icon !== 'string') { + collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'icon')); + return false; + } + + return true; + } +}