diff --git a/extensions/git-extended/src/review/reviewMode.ts b/extensions/git-extended/src/review/reviewMode.ts index 7662c9475db..8d6f0af91cb 100644 --- a/extensions/git-extended/src/review/reviewMode.ts +++ b/extensions/git-extended/src/review/reviewMode.ts @@ -136,6 +136,40 @@ export class ReviewMode { commentsCache.set(changedItem.filePath.toString(), matchingComments); }); + function commentsToCommentThreads(uri: vscode.Uri, comments: Comment[]): vscode.CommentThread[] { + if (!comments || !comments.length) { + return []; + } + + let sections = _.groupBy(comments, comment => comment.position); + let ret: vscode.CommentThread[] = []; + + for (let i in sections) { + let comments = sections[i]; + + const comment = comments[0]; + const commentAbsolutePosition = comment.diff_hunk_range.start + (comment.position - 1); + const pos = new vscode.Position(comment.currentPosition ? comment.currentPosition - 1 - 1 : commentAbsolutePosition - /* after line */ 1 - /* it's zero based*/ 1, 0); + const range = new vscode.Range(pos, pos); + + ret.push({ + threadId: comment.id, + resource: uri, + range, + comments: comments.map(comment => { + return { + body: new vscode.MarkdownString(comment.body), + userName: comment.user.login, + gravatar: comment.user.avatar_url + }; + }), + actions: actions + }); + } + + return ret; + } + this._commentProvider = vscode.workspace.registerCommentProvider({ provideNewCommentRange: async (document: vscode.TextDocument, token: vscode.CancellationToken) => { if (document.uri.scheme === 'review' || document.uri.scheme === 'file') { @@ -174,36 +208,12 @@ export class ReviewMode { } } - if (!matchingComments || !matchingComments.length) { - return []; - } - - let sections = _.groupBy(matchingComments, comment => comment.position); - let ret: vscode.CommentThread[] = []; - - for (let i in sections) { - let comments = sections[i]; - - const comment = comments[0]; - const commentAbsolutePosition = comment.diff_hunk_range.start + (comment.position - 1); - const pos = new vscode.Position(comment.currentPosition ? comment.currentPosition - 1 - 1 : commentAbsolutePosition - /* after line */ 1 - /* it's zero based*/ 1, 0); - const range = new vscode.Range(pos, pos); - - ret.push({ - threadId: comment.id, - range, - comments: comments.map(comment => { - return { - body: new vscode.MarkdownString(comment.body), - userName: comment.user.login, - gravatar: comment.user.avatar_url - }; - }), - actions: actions - }); - } - - return ret; + return commentsToCommentThreads(document.uri, matchingComments); + }, + provideAllComments: async (token: vscode.CancellationToken) => { + return fileChanges + .map(fileChange => commentsToCommentThreads(fileChange.filePath, fileChange.comments)) + .reduce((prev, curr) => prev.concat(curr), []); } }); diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index cd85a7670ef..56eeb3b4c9f 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -940,6 +940,7 @@ export interface Command { export interface CommentThread { readonly threadId: string; + readonly resource: string; readonly range: IRange; readonly comments: Comment[]; readonly actions: Command[]; @@ -959,6 +960,7 @@ export interface Comment { export interface CommentProvider { provideComments(model: model.ITextModel, token: CancellationToken): CommentThread[]; provideNewCommentRange(model: model.ITextModel, token: CancellationToken): Promise; + provideAllComments(token: CancellationToken): Promise; } export interface ICodeLensSymbol { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index dc21a6dab00..6a02849802f 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5030,6 +5030,7 @@ declare namespace monaco.languages { export interface CommentThread { readonly threadId: string; + readonly resource: string; readonly range: IRange; readonly comments: Comment[]; readonly actions: Command[]; @@ -5049,6 +5050,7 @@ declare namespace monaco.languages { export interface CommentProvider { provideComments(model: editor.ITextModel, token: CancellationToken): CommentThread[]; provideNewCommentRange(model: editor.ITextModel, token: CancellationToken): Promise; + provideAllComments(token: CancellationToken): Promise; } export interface ICodeLensSymbol { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index af6baa70f81..c6a09c6db2b 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -772,6 +772,7 @@ declare module 'vscode' { interface CommentThread { threadId: string; + resource: Uri; range: Range; comments: Comment[]; actions?: Command[]; @@ -795,6 +796,7 @@ declare module 'vscode' { interface CommentProvider { provideComments(document: TextDocument, token: CancellationToken): Promise; provideNewCommentRange(document: TextDocument, token: CancellationToken): Promise; + provideAllComments?(token: CancellationToken): Promise; } namespace workspace { diff --git a/src/vs/workbench/api/electron-browser/mainThreadComments.ts b/src/vs/workbench/api/electron-browser/mainThreadComments.ts index a8254786648..fd6ee4840e9 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadComments.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadComments.ts @@ -50,8 +50,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments const outerEditorURI = outerEditor.getModel().uri; this.provideComments(outerEditorURI).then(commentThreads => { - controller.setComments(commentThreads); - this._commentService.setCommentsForResource(outerEditorURI, commentThreads); + this._commentService.setComments(outerEditorURI, commentThreads); }); this.provideNewCommentRange(outerEditor.getModel()).then(newActions => { controller.setNewCommentActions(newActions); @@ -61,11 +60,20 @@ export class MainThreadComments extends Disposable implements MainThreadComments $registerCommentProvider(handle: number): void { this._providers.set(handle, undefined); - this._panelService.setPanelEnablement(COMMENTS_PANEL_ID, true); + // Fetch all comments + this._proxy.$provideAllComments(handle).then(commentThreads => { + if (commentThreads) { + this._commentService.setAllComments(commentThreads); + this._panelService.setPanelEnablement(COMMENTS_PANEL_ID, true); + } + }); + + } $unregisterCommentProvider(handle: number): void { this._providers.delete(handle); + this._panelService.setPanelEnablement(COMMENTS_PANEL_ID, false); } dispose(): void { diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 140947eb779..3d1d6c9683e 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -833,6 +833,7 @@ export interface ExtHostProgressShape { export interface ExtHostCommentsShape { $provideComments(handle: number, document: UriComponents): TPromise; $provideNewCommentRange(handle: number, document: UriComponents): TPromise; + $provideAllComments(handle: number): TPromise; } // --- proxy identifiers diff --git a/src/vs/workbench/api/node/extHostComments.ts b/src/vs/workbench/api/node/extHostComments.ts index 56ff85d9d66..a1804f7ab3b 100644 --- a/src/vs/workbench/api/node/extHostComments.ts +++ b/src/vs/workbench/api/node/extHostComments.ts @@ -58,6 +58,22 @@ export class ExtHostComments implements ExtHostCommentsShape { .then(comments => comments.map(x => convertCommentThread(x, this._commandsConverter))); } + $provideAllComments(handle: number): TPromise { + let provider = this._providers.get(handle); + // provideAllComments is an optional method + if (!provider.provideAllComments) { + return TPromise.as(null); + } + + return asWinJsPromise(token => { + + return provider.provideAllComments(token); + }).then(comments => + comments.map(x => convertCommentThread(x, this._commandsConverter) + )); + } + + $provideNewCommentRange(handle: number, uri: UriComponents): TPromise { const data = this._documents.getDocumentData(URI.revive(uri)); if (!data || !data.document) { @@ -87,6 +103,7 @@ function convertNewCommandAction(vscodeNewCommentAction: vscode.NewCommentAction function convertCommentThread(vscodeCommentThread: vscode.CommentThread, commandsConverter: CommandsConverter): modes.CommentThread { return { threadId: vscodeCommentThread.threadId, + resource: vscodeCommentThread.resource.toString(), range: extHostTypeConverter.fromRange(vscodeCommentThread.range), comments: vscodeCommentThread.comments.map(convertComment), actions: vscodeCommentThread.actions.map(commandsConverter.toInternal) diff --git a/src/vs/workbench/parts/comments/common/commentModel.ts b/src/vs/workbench/parts/comments/common/commentModel.ts new file mode 100644 index 00000000000..f404d8e5389 --- /dev/null +++ b/src/vs/workbench/parts/comments/common/commentModel.ts @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * 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 URI from 'vs/base/common/uri'; +import { IRange } from 'vs/editor/common/core/range'; +import { Comment, CommentThread } from 'vs/editor/common/modes'; +import { groupBy } from 'vs/base/common/arrays'; + +export class CommentNode { + threadId: string; + range: IRange; + comment: Comment; + reply: CommentNode; + resource: URI; + + constructor(threadId: string, resource: URI, comment: Comment, range: IRange) { + this.threadId = threadId; + this.comment = comment; + this.resource = resource; + this.range = range; + } + + hasReply(): boolean { + return !!this.reply; + } +} + +export class ResourceCommentThreads { + id: string; + comments: CommentNode[]; // The top level comments on the file. Replys are nested under each node. + resource: URI; + + constructor(resource: URI, commentThreads: CommentThread[]) { + this.id = resource.toString(); + this.resource = resource; + this.comments = commentThreads.map(thread => this.createCommentNode(resource, thread)); + } + + private createCommentNode(resource: URI, commentThread: CommentThread): CommentNode { + const { threadId, comments, range } = commentThread; + const commentNodes: CommentNode[] = comments.map(comment => new CommentNode(threadId, resource, comment, range)); + for (var i = 0; i < commentNodes.length - 1; i++) { + const commentNode = commentNodes[i]; + commentNode.reply = commentNodes[i + 1]; + } + + commentNodes.push(new CommentNode(threadId, resource, comments[comments.length - 1], range)); + + return commentNodes[0]; + } +} + +export class CommentsModel { + commentThreads: ResourceCommentThreads[]; + commentThreadsByResource: Map; + + constructor() { + this.commentThreads = []; + this.commentThreadsByResource = new Map(); + } + + + public setCommentThreadsForResource(resource: URI, commentThreads: CommentThread[]): boolean { + if (!commentThreads.length || this.commentThreadsByResource.get(resource.toString())) { + return false; + } + + this.commentThreadsByResource.set(resource.toString(), new ResourceCommentThreads(resource, commentThreads)); + this.commentThreads = []; + this.commentThreadsByResource.forEach((v, i, m) => { + this.commentThreads.push(v); + }); + + return true; + } + + public setCommentThreads(commentThreads: CommentThread[]) { + for (const group of groupBy(commentThreads, CommentsModel._compareURIs)) { + this.commentThreadsByResource.set(group[0].resource, new ResourceCommentThreads(URI.parse(group[0].resource), group)); + } + + this.commentThreadsByResource.forEach((v, i, m) => { + this.commentThreads.push(v); + }); + } + + private static _compareURIs(a: CommentThread, b: CommentThread) { + const resourceA = a.resource.toString(); + const resourceB = b.resource.toString(); + if (resourceA < resourceB) { + return -1; + } else if (resourceA > resourceB) { + return 1; + } else { + return 0; + } + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts b/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts index 3437e3fec05..53118a6684a 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts @@ -33,6 +33,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry'; import { ZoneWidget, IOptions } from 'vs/editor/contrib/zoneWidget/zoneWidget'; import { ReviewModel, ReviewStyle } from 'vs/workbench/parts/comments/common/reviewModel'; +import { ICommentService } from '../../../services/comments/electron-browser/commentService'; export const ctxReviewPanelVisible = new RawContextKey('reviewPanelVisible', false); export const ID = 'editor.contrib.review'; @@ -291,6 +292,7 @@ export class ReviewController implements IEditorContribution { @IContextKeyService contextKeyService: IContextKeyService, @IThemeService private themeService: IThemeService, @ICommandService private commandService: ICommandService, + @ICommentService private commentService: ICommentService ) { this.editor = editor; this.globalToDispose = []; @@ -343,6 +345,13 @@ export class ReviewController implements IEditorContribution { } }); + this.commentService.onDidSetResourceCommentThreads(e => { + const editorURI = this.editor && this.editor.getModel() && this.editor.getModel().uri; + if (editorURI && editorURI.toString() === e.resource.toString()) { + this.setComments(e.commentThreads); + } + }); + this.globalToDispose.push(this.editor.onDidChangeModel(() => this.onModelChanged())); } @@ -449,6 +458,7 @@ export class ReviewController implements IEditorContribution { }); this._zoneWidget.display({ threadId: null, + resource: null, comments: [], range: { startLineNumber: lineNumber, diff --git a/src/vs/workbench/parts/comments/electron-browser/commentsPanel.ts b/src/vs/workbench/parts/comments/electron-browser/commentsPanel.ts index cdc9d969075..279abb8923b 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentsPanel.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentsPanel.ts @@ -16,8 +16,10 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ResourceLabel } from 'vs/workbench/browser/labels'; import { Panel } from 'vs/workbench/browser/panel'; -import { CommentNode, CommentsModel, ICommentService, ResourceCommentThreads } from 'vs/workbench/services/comments/electron-browser/commentService'; import { ReviewController } from 'vs/workbench/parts/comments/electron-browser/commentsEditorContribution'; +import { ICommentService } from 'vs/workbench/services/comments/electron-browser/commentService'; +import { ResourceCommentThreads, CommentsModel, CommentNode } from 'vs/workbench/parts/comments/common/commentModel'; +import { CommentThread } from 'vs/editor/common/modes'; export const COMMENTS_PANEL_ID = 'workbench.panel.comments'; export const COMMENTS_PANEL_TITLE = 'Comments'; @@ -153,6 +155,7 @@ interface IResourceMarkersTemplateData { export class CommentsPanel extends Panel { private tree: WorkbenchTree; private treeContainer: HTMLElement; + private commentsModel: CommentsModel; constructor( @IInstantiationService private instantiationService: IInstantiationService, @@ -171,15 +174,17 @@ export class CommentsPanel extends Panel { let container = dom.append(parent, dom.$('.markers-panel-container')); this.treeContainer = dom.append(container, dom.$('.tree-container')); + this.commentsModel = new CommentsModel(); this.createTree(); - this.commentService.onDidChangeCommentThreads(this.onCommentThreadChanged, this); + this.commentService.onDidSetAllCommentThreads(this.onAllCommentsChanged, this); return this.render(); } - private onCommentThreadChanged() { + private onAllCommentsChanged(e: CommentThread[]) { + this.commentsModel.setCommentThreads(e); this.tree.refresh().then(() => { console.log('tree refreshed'); }, (e) => { @@ -189,7 +194,7 @@ export class CommentsPanel extends Panel { private render(): TPromise { dom.toggleClass(this.treeContainer, 'hidden', false); - return this.tree.setInput(this.commentService.commentsModel); + return this.tree.setInput(this.commentsModel); } public layout(dimensions: dom.Dimension): void { diff --git a/src/vs/workbench/services/comments/electron-browser/commentService.ts b/src/vs/workbench/services/comments/electron-browser/commentService.ts index 20b9baa5b99..0115a8074f1 100644 --- a/src/vs/workbench/services/comments/electron-browser/commentService.ts +++ b/src/vs/workbench/services/comments/electron-browser/commentService.ts @@ -5,111 +5,46 @@ 'use strict'; - -import { TPromise } from 'vs/base/common/winjs.base'; -import { CommentThread, Comment } from 'vs/editor/common/modes'; +import { CommentThread } 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'; import URI from 'vs/base/common/uri'; -import { IRange } from 'vs/editor/common/core/range'; export const ICommentService = createDecorator('commentService'); -export class CommentNode { - threadId: string; - range: IRange; - comment: Comment; - reply: CommentNode; +export interface IResourceCommentThreadEvent { resource: URI; - - constructor(threadId: string, resource: URI, comment: Comment, range: IRange) { - this.threadId = threadId; - this.comment = comment; - this.resource = resource; - this.range = range; - } - - hasReply(): boolean { - return !!this.reply; - } -} - -export class ResourceCommentThreads { - id: string; - comments: CommentNode[]; // The top level comments on the file. Replys are nested under each node. - resource: URI; - - constructor(resource: URI, commentThreads: CommentThread[]) { - this.id = resource.toString(); - this.resource = resource; - this.comments = commentThreads.map(thread => this.createCommentNode(resource, thread)); - } - - private createCommentNode(resource: URI, commentThread: CommentThread): CommentNode { - const { threadId, comments, range } = commentThread; - const commentNodes: CommentNode[] = comments.map(comment => new CommentNode(threadId, resource, comment, range)); - for (var i = 0; i < commentNodes.length - 1; i++) { - const commentNode = commentNodes[i]; - commentNode.reply = commentNodes[i + 1]; - } - - commentNodes.push(new CommentNode(threadId, resource, comments[comments.length - 1], range)); - - return commentNodes[0]; - } -} - -export class CommentsModel { - commentThreads: ResourceCommentThreads[]; - commentThreadsByResource: Map; - - constructor() { - this.commentThreads = []; - this.commentThreadsByResource = new Map(); - } - - - public setCommentThreadsForResource(resource: URI, commentThreads: CommentThread[]): boolean { - if (!commentThreads.length || this.commentThreadsByResource.get(resource.toString())) { - return false; - } - - this.commentThreadsByResource.set(resource.toString(), new ResourceCommentThreads(resource, commentThreads)); - this.commentThreads = []; - this.commentThreadsByResource.forEach((v, i, m) => { - this.commentThreads.push(v); - }); - - return true; - } + commentThreads: CommentThread[]; } export interface ICommentService { _serviceBrand: any; - readonly commentsModel; - readonly onDidChangeCommentThreads: Event; - setCommentsForResource(resource: URI, commentThreads: CommentThread[]); + readonly onDidSetResourceCommentThreads: Event; + readonly onDidSetAllCommentThreads: Event; + setComments(resource: URI, commentThreads: CommentThread[]): void; + setAllComments(commentsByResource: CommentThread[]): void; } export class CommentService extends Disposable implements ICommentService { _serviceBrand: any; - commentsModel: CommentsModel; + private readonly _onDidSetResourceCommentThreads: Emitter = this._register(new Emitter()); + readonly onDidSetResourceCommentThreads: Event = this._onDidSetResourceCommentThreads.event; - private readonly _onDidChangeCommentThreads: Emitter = this._register(new Emitter()); - readonly onDidChangeCommentThreads: Event = this._onDidChangeCommentThreads.event; + private readonly _onDidSetAllCommentThreads: Emitter = this._register(new Emitter()); + readonly onDidSetAllCommentThreads: Event = this._onDidSetAllCommentThreads.event; constructor() { super(); - this.commentsModel = new CommentsModel(); } - setCommentsForResource(resource: URI, commentThreads: CommentThread[]): TPromise { - if (this.commentsModel.setCommentThreadsForResource(resource, commentThreads)) { - this._onDidChangeCommentThreads.fire(); - } - return TPromise.as(null); + setComments(resource: URI, commentThreads: CommentThread[]): void { + this._onDidSetResourceCommentThreads.fire({ resource, commentThreads }); + } + + setAllComments(commentsByResource: CommentThread[]): void { + this._onDidSetAllCommentThreads.fire(commentsByResource); } }