diff --git a/src/vs/workbench/api/electron-browser/mainThreadComments.ts b/src/vs/workbench/api/electron-browser/mainThreadComments.ts index 4eaf10e729d..5886e39130c 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadComments.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadComments.ts @@ -16,6 +16,10 @@ import { keys } from '../../../base/common/map'; import { IWorkbenchEditorService } from '../../services/editor/common/editorService'; import { ExtHostCommentsShape, ExtHostContext, IExtHostContext, MainContext, MainThreadCommentsShape } from '../node/extHost.protocol'; +import { ICommentService } from 'vs/workbench/services/comments/electron-browser/commentService'; +import { COMMENTS_PANEL_ID } from 'vs/workbench/parts/comments/electron-browser/commentsPanel'; +import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; + @extHostNamedCustomer(MainContext.MainThreadComments) export class MainThreadComments extends Disposable implements MainThreadCommentsShape { @@ -26,6 +30,8 @@ export class MainThreadComments extends Disposable implements MainThreadComments extHostContext: IExtHostContext, @IEditorGroupService editorGroupService: IEditorGroupService, @IWorkbenchEditorService private _workbenchEditorService: IWorkbenchEditorService, + @ICommentService private _commentService: ICommentService, + @IPanelService private _panelService: IPanelService, @ICodeEditorService private _codeEditorService: ICodeEditorService ) { super(); @@ -43,12 +49,14 @@ export class MainThreadComments extends Disposable implements MainThreadComments this.provideComments(outerEditor.getModel()).then(commentThreads => { controller.setComments(commentThreads); + this._commentService.updateComments(commentThreads); }); }); } $registerCommentProvider(handle: number): void { this._providers.set(handle, undefined); + this._panelService.setPanelEnablement(COMMENTS_PANEL_ID, true); } $unregisterCommentProvider(handle: number): void { @@ -68,7 +76,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments return editor; } - async provideComments(model: ITextModel): Promise { + async provideComments(model: ITextModel): Promise { const result: modes.CommentThread[] = []; for (const handle of keys(this._providers)) { result.push(...await this._proxy.$providerComments(handle, model.uri)); diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index cdb6879f8a6..9c945b6ddd4 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -108,6 +108,7 @@ import { registerNotificationCommands } from 'vs/workbench/browser/parts/notific import { NotificationsToasts } from 'vs/workbench/browser/parts/notifications/notificationsToasts'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { PreferencesService } from 'vs/workbench/services/preferences/browser/preferencesService'; +import { ICommentService, CommentService } from '../services/comments/electron-browser/commentService'; export const EditorsVisibleContext = new RawContextKey('editorIsOpen', false); export const InZenModeContext = new RawContextKey('inZenMode', false); @@ -590,7 +591,7 @@ export class Workbench implements IPartService { this.titlebarPart = this.instantiationService.createInstance(TitlebarPart, Identifiers.TITLEBAR_PART); this.toUnbind.push({ dispose: () => this.titlebarPart.shutdown() }); serviceCollection.set(ITitleService, this.titlebarPart); - + serviceCollection.set(ICommentService, new SyncDescriptor(CommentService)); // History serviceCollection.set(IHistoryService, new SyncDescriptor(HistoryService)); diff --git a/src/vs/workbench/parts/comments/electron-browser/comments.contribution.ts b/src/vs/workbench/parts/comments/electron-browser/comments.contribution.ts new file mode 100644 index 00000000000..3b45b0748fe --- /dev/null +++ b/src/vs/workbench/parts/comments/electron-browser/comments.contribution.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PanelRegistry, Extensions as PanelExtensions, PanelDescriptor } from 'vs/workbench/browser/panel'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { CommentsPanel, COMMENTS_PANEL_ID, COMMENTS_PANEL_TITLE } from './commentsPanel'; +import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; + +export class CommentPanelVisibilityUpdater implements IWorkbenchContribution { + + constructor( + @IPanelService panelService: IPanelService + ) { + // commentsProviderRegistry.onChange + const updateCommentPanelVisibility = () => { + panelService.setPanelEnablement(COMMENTS_PANEL_ID, false); + }; + + updateCommentPanelVisibility(); + } +} + +Registry.as(PanelExtensions.Panels).registerPanel(new PanelDescriptor( + CommentsPanel, + COMMENTS_PANEL_ID, + COMMENTS_PANEL_TITLE, + 'commentsPanel', + 10 +)); + +// Register view location updater +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(CommentPanelVisibilityUpdater, LifecyclePhase.Running); diff --git a/src/vs/workbench/parts/comments/electron-browser/commentsPanel.ts b/src/vs/workbench/parts/comments/electron-browser/commentsPanel.ts new file mode 100644 index 00000000000..91334eb5dbb --- /dev/null +++ b/src/vs/workbench/parts/comments/electron-browser/commentsPanel.ts @@ -0,0 +1,199 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { Panel } from 'vs/workbench/browser/panel'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import * as dom from 'vs/base/browser/dom'; +import { WorkbenchTree } from '../../../../platform/list/browser/listService'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation'; +import { IDataSource, ITree, IRenderer } from 'vs/base/parts/tree/browser/tree'; +import { TPromise, Promise } from 'vs/base/common/winjs.base'; +import { ICommentService, CommentsModel, ResourceCommentThread, CommentNode } from 'vs/workbench/services/comments/electron-browser/commentService'; +import { ResourceLabel } from 'vs/workbench/browser/labels'; +import { DefaultAccessibilityProvider, DefaultController, DefaultDragAndDrop, DefaultFilter } from '../../../../base/parts/tree/browser/treeDefaults'; + +export const COMMENTS_PANEL_ID = 'workbench.panel.comments'; +export const COMMENTS_PANEL_TITLE = 'Comments'; + +export class CommentsDataSource implements IDataSource { + public getId(tree: ITree, element: any): string { + if (element instanceof CommentsModel) { + return 'root'; + } + if (element instanceof ResourceCommentThread) { + return element.id; + } + if (element instanceof CommentNode) { + return 'commentNode' + Math.random().toString(); + } + return ''; + } + + public hasChildren(tree: ITree, element: any): boolean { + return element instanceof CommentsModel || element instanceof ResourceCommentThread || (element instanceof CommentNode && element.hasReply()); + } + + public getChildren(tree: ITree, element: any): Promise { + if (element instanceof CommentsModel) { + return Promise.as(element.commentThreads); + } + if (element instanceof ResourceCommentThread) { + return Promise.as([element.comments[0]]); + } + if (element instanceof CommentNode && element.hasReply()) { + return Promise.as([element.reply]); + } + return null; + } + + public getParent(tree: ITree, element: any): Promise { + return TPromise.as(null); + } + + public shouldAutoexpand(tree: ITree, element: any): boolean { + return true; + } +} + +export class CommentsModelRenderer implements IRenderer { + private static COMMENTS_THREAD_ID = 'comments-thread'; + private static COMMENT_ID = 'comment'; + + + constructor( + @IInstantiationService private instantiationService: IInstantiationService, + ) { + } + + public getHeight(tree: ITree, element: any): number { + return 22; + } + + public getTemplateId(tree: ITree, element: any): string { + if (element instanceof ResourceCommentThread) { + return CommentsModelRenderer.COMMENTS_THREAD_ID; + } + if (element instanceof CommentNode) { + return CommentsModelRenderer.COMMENT_ID; + } + + return ''; + } + + public renderTemplate(ITree: ITree, templateId: string, container: HTMLElement): any { + switch (templateId) { + case CommentsModelRenderer.COMMENTS_THREAD_ID: + return this.renderCommentsThreadTemplate(container); + case CommentsModelRenderer.COMMENT_ID: + return this.renderCommentTemplate(container); + } + } + + public disposeTemplate(tree: ITree, templateId: string): void { + // TODO + } + + public renderElement(tree: ITree, element: any, templateId: string, templateData: any): void { + switch (templateId) { + case CommentsModelRenderer.COMMENTS_THREAD_ID: + return this.renderCommentsThreadElement(tree, element, templateData); + case CommentsModelRenderer.COMMENT_ID: + return this.renderCommentElement(tree, element, templateData); + } + } + + private renderCommentsThreadTemplate(container: HTMLElement): IResourceMarkersTemplateData { + const data = Object.create(null); + const labelContainer = dom.append(container, dom.$('.comment-thread-container')); + data.resourceLabel = this.instantiationService.createInstance(ResourceLabel, labelContainer, {}); + + return data; + } + + private renderCommentTemplate(container: HTMLElement): IResourceMarkersTemplateData { + const data = Object.create(null); + const labelContainer = dom.append(container, dom.$('.comment-container')); + data.resourceLabel = this.instantiationService.createInstance(ResourceLabel, labelContainer, {}); + + return data; + } + + private renderCommentsThreadElement(tree: ITree, element: ResourceCommentThread, templateData: IResourceMarkersTemplateData) { + templateData.resourceLabel.setLabel({ name: element.id }); + } + + private renderCommentElement(tree: ITree, element: CommentNode, templateData: IResourceMarkersTemplateData) { + templateData.resourceLabel.setLabel({ name: element.comment.body.value }); + } +} + +interface IResourceMarkersTemplateData { + resourceLabel: ResourceLabel; +} +export class CommentsPanel extends Panel { + private tree: WorkbenchTree; + private treeContainer: HTMLElement; + + constructor( + @IInstantiationService private instantiationService: IInstantiationService, + @ICommentService private commentService: ICommentService, + @ITelemetryService telemetryService: ITelemetryService, + @IThemeService themeService: IThemeService, + ) { + super(COMMENTS_PANEL_ID, telemetryService, themeService); + } + + public create(parent: HTMLElement): TPromise { + super.create(parent); + + dom.addClass(parent, 'markers-panel'); + + let container = dom.append(parent, dom.$('.markers-panel-container')); + this.treeContainer = dom.append(container, dom.$('.tree-container')); + + this.createTree(); + + this.commentService.onDidChangeCommentThreads(this.onCommentThreadChanged, this); + + return this.render(); + } + + private onCommentThreadChanged() { + this.tree.refresh().then(() => { + console.log('tree refreshed'); + }, (e) => { + console.log(e); + }); + } + + private render(): TPromise { + dom.toggleClass(this.treeContainer, 'hidden', false); + console.log(this.commentService.commentsModel); + return this.tree.setInput(this.commentService.commentsModel); + } + + public layout(dimensions: dom.Dimension): void { + this.tree.layout(dimensions.height, dimensions.width); + } + + public getTitle(): string { + return COMMENTS_PANEL_TITLE; + } + + private createTree(): void { + this.tree = this.instantiationService.createInstance(WorkbenchTree, this.treeContainer, { + dataSource: new CommentsDataSource(), + renderer: new CommentsModelRenderer(this.instantiationService), + accessibilityProvider: new DefaultAccessibilityProvider, + controller: new DefaultController(), + dnd: new DefaultDragAndDrop(), + filter: new DefaultFilter() + }, { + twistiePixels: 20, + ariaLabel: COMMENTS_PANEL_TITLE + }); + } +} diff --git a/src/vs/workbench/services/comments/electron-browser/commentService.ts b/src/vs/workbench/services/comments/electron-browser/commentService.ts new file mode 100644 index 00000000000..8c90291162a --- /dev/null +++ b/src/vs/workbench/services/comments/electron-browser/commentService.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + + +import { TPromise } from 'vs/base/common/winjs.base'; +import { CommentThread, Comment } from 'vs/editor/common/modes'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { Event, Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; + +export const ICommentService = createDecorator('commentService'); + +export class CommentNode { + comment: Comment; + reply: CommentNode; + + constructor(comment: Comment) { + this.comment = comment; + } + + hasReply(): boolean { + return !!this.reply; + } +} + +export class ResourceCommentThread { + id: string; + comments: CommentNode[]; + + constructor(commentThread: CommentThread) { + this.id = commentThread.threadId; + this.comments = this.createCommentNodes(commentThread.comments); + } + + private createCommentNodes(comments: Comment[]): CommentNode[] { + const commentNodes: CommentNode[] = comments.map(comment => new CommentNode(comment)); + for (var i = 0; i < commentNodes.length - 1; i++) { + const commentNode = commentNodes[i]; + commentNode.reply = commentNodes[i + 1]; + } + + commentNodes.push(new CommentNode(comments[comments.length - 1])); + + return commentNodes; + } +} + +export class CommentsModel { + commentThreads: ResourceCommentThread[]; + + constructor() { + this.commentThreads = []; + } + + // TODO: Should have an additional level of nesting, mapping file to comment threads + public setCommentThreads(commentThreads: CommentThread[]) { + this.commentThreads = commentThreads.map(commentThread => new ResourceCommentThread(commentThread)); + } +} + +export interface ICommentService { + _serviceBrand: any; + readonly commentsModel; + readonly onDidChangeCommentThreads: Event; + updateComments(commentThreads: CommentThread[]); +} + +export class CommentService extends Disposable implements ICommentService { + _serviceBrand: any; + + commentsModel: CommentsModel; + + private readonly _onDidChangeCommentThreads: Emitter = this._register(new Emitter()); + readonly onDidChangeCommentThreads: Event = this._onDidChangeCommentThreads.event; + + constructor() { + super(); + this.commentsModel = new CommentsModel(); + } + + updateComments(commentThreads: CommentThread[]): TPromise { + this.commentsModel.setCommentThreads(commentThreads); + this._onDidChangeCommentThreads.fire(); + return TPromise.as(null); + } +} diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index 62d2302178b..4b97fd30670 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -64,6 +64,7 @@ import 'vs/workbench/parts/debug/browser/debugEditorActions'; import 'vs/workbench/parts/debug/browser/debugViewlet'; // can be packaged separately import 'vs/workbench/parts/markers/electron-browser/markers.contribution'; +import 'vs/workbench/parts/comments/electron-browser/comments.contribution'; import 'vs/workbench/parts/html/electron-browser/html.contribution';