diff --git a/extensions/git-extended/src/prView/prProvider.ts b/extensions/git-extended/src/prView/prProvider.ts index 5aa87a246f6..587f428a2b7 100644 --- a/extensions/git-extended/src/prView/prProvider.ts +++ b/extensions/git-extended/src/prView/prProvider.ts @@ -126,46 +126,54 @@ export class PRProvider implements vscode.TreeDataProvider { - if (thread && thread.threadId) { - try { - let ret = await element.createCommentReply(text, thread.threadId); - return { - body: new vscode.MarkdownString(ret.data.body), - userName: ret.data.user.login, - gravatar: ret.data.user.avatar_url - }; - } catch (e) { - return null; - } - } else { - let params = JSON.parse(uri.query); + let createNewCommentThread = async (document: vscode.TextDocument, range: vscode.Range, text: string) => { + let uri = document.uri; + let params = JSON.parse(uri.query); - let fileChange = richContentChanges.find(change => change.fileName === params.fileName); + let fileChange = richContentChanges.find(change => change.fileName === params.fileName); - if (!fileChange) { - return null; - } + if (!fileChange) { + return null; + } - let position = mapHeadLineToDiffHunkPosition(fileChange.patch, '', range.start.line); + let position = mapHeadLineToDiffHunkPosition(fileChange.patch, '', range.start.line); - if (position < 0) { - return; - } + if (position < 0) { + return; + } - // there is no thread Id, which means it's a new thread - let ret = await element.createComment(text, params.fileName, position); - return { + // there is no thread Id, which means it's a new thread + let ret = await element.createComment(text, params.fileName, position); + let comment: vscode.Comment = { + commentId: ret.data.id, + body: new vscode.MarkdownString(ret.data.body), + userName: ret.data.user.login, + gravatar: ret.data.user.avatar_url + }; + + let commentThread: vscode.CommentThread = { + threadId: comment.commentId, + resource: uri, + range: range, + comments: [comment] + }; + + return commentThread; + }; + + let replyToCommentThread = async (uri: vscode.Uri, range: vscode.Range, thread: vscode.CommentThread, text: string) => { + try { + let ret = await element.createCommentReply(text, thread.threadId); + thread.comments.push({ commentId: ret.data.id, body: new vscode.MarkdownString(ret.data.body), userName: ret.data.user.login, gravatar: ret.data.user.avatar_url - }; + }); + return thread; + } catch (e) { + return null; } - }); - let reply = { - command: 'diff-' + element.prItem.number + '-post', - title: 'Add comment' }; const _onDidChangeCommentThreads = new vscode.EventEmitter(); @@ -196,7 +204,6 @@ export class PRProvider implements vscode.TreeDataProvider; private _localFileChanges: FileChangeTreeItem[] = []; - private _reply: vscode.Command = null; private _lastCommitSha: string; private _onDidChangeCommentThreads = new vscode.EventEmitter(); @@ -161,50 +160,6 @@ export class ReviewManager implements vscode.DecorationProvider { await this.getPullRequestData(pr); await this.prFileChangesProvider.showPullRequestFileChanges(this._localFileChanges); - this._command = vscode.commands.registerCommand(this._prNumber + '-post', async (uri: vscode.Uri, range: vscode.Range, thread: vscode.CommentThread, text: string) => { - try { - if (thread && thread.threadId) { - let ret = await pr.createCommentReply(text, thread.threadId); - return { - commentId: ret.data.id, - body: new vscode.MarkdownString(ret.data.body), - userName: ret.data.user.login, - gravatar: ret.data.user.avatar_url - }; - } else { - let fileName = uri.path; - let matchedFiles = this._localFileChanges.filter(fileChange => path.resolve(this._repository.path, fileChange.fileName) === fileName); - if (matchedFiles && matchedFiles.length) { - let matchedFile = matchedFiles[0]; - // git diff sha -- fileName - let contentDiff = await this._repository.diff(matchedFile.fileName, this._lastCommitSha); - let position = mapHeadLineToDiffHunkPosition(matchedFile.patch, contentDiff, range.start.line); - - if (position < 0) { - return; - } - - // there is no thread Id, which means it's a new thread - let ret = await pr.createComment(text, matchedFile.fileName, position); - - return { - commentId: ret.data.id, - body: new vscode.MarkdownString(ret.data.body), - userName: ret.data.user.login, - gravatar: ret.data.user.avatar_url - }; - } - } - } catch (e) { - return null; - } - }); - - this._reply = { - command: this._prNumber + '-post', - title: 'Add comment' - }; - this._onDidChangeDecorations.fire(); this.registerCommentProvider(); @@ -214,6 +169,59 @@ export class ReviewManager implements vscode.DecorationProvider { vscode.commands.executeCommand('pr.refreshList'); } + private async replyToCommentThread(document: vscode.TextDocument, range: vscode.Range, thread: vscode.CommentThread, text: string) { + try { + let ret = await this._pr.createCommentReply(text, thread.threadId); + thread.comments.push({ + commentId: ret.data.id, + body: new vscode.MarkdownString(ret.data.body), + userName: ret.data.user.login, + gravatar: ret.data.user.avatar_url + }); + return thread; + } catch (e) { + return null; + } + } + private async createNewCommentThread(document: vscode.TextDocument, range: vscode.Range, text: string) { + try { + let uri = document.uri; + let fileName = uri.path; + let matchedFiles = this._localFileChanges.filter(fileChange => path.resolve(this._repository.path, fileChange.fileName) === fileName); + if (matchedFiles && matchedFiles.length) { + let matchedFile = matchedFiles[0]; + // git diff sha -- fileName + let contentDiff = await this._repository.diff(matchedFile.fileName, this._lastCommitSha); + let position = mapHeadLineToDiffHunkPosition(matchedFile.patch, contentDiff, range.start.line); + + if (position < 0) { + return; + } + + // there is no thread Id, which means it's a new thread + let ret = await this._pr.createComment(text, matchedFile.fileName, position); + + let comment = { + commentId: ret.data.id, + body: new vscode.MarkdownString(ret.data.body), + userName: ret.data.user.login, + gravatar: ret.data.user.avatar_url + }; + + let commentThread: vscode.CommentThread = { + threadId: comment.commentId, + resource: uri, + range: range, + comments: [comment] + }; + + return commentThread; + } + } catch (e) { + return null; + } + } + private async updateComments(): Promise { const branch = this._repository.HEAD.name; const state = this._workspaceState.get(`${REVIEW_STATE}:${branch}`) as ReviewState; @@ -363,8 +371,7 @@ export class ReviewManager implements vscode.DecorationProvider { gravatar: comment.user.avatar_url }; }), - collapsibleState: collapsibleState, - postReviewComment: this._reply + collapsibleState: collapsibleState }); } @@ -473,9 +480,10 @@ export class ReviewManager implements vscode.DecorationProvider { return { threads: this.commentsToCommentThreads(matchingComments, document.uri.scheme === 'file' ? vscode.CommentThreadCollapsibleState.Collapsed : vscode.CommentThreadCollapsibleState.Expanded), commentingRanges: ranges, - postReviewComment: this._reply }; - } + }, + createNewCommentThread: this.createNewCommentThread.bind(this), + replyToCommentThread: this.replyToCommentThread.bind(this) }); this._workspaceCommentProvider = vscode.workspace.registerWorkspaceCommentProvider({ @@ -485,7 +493,9 @@ export class ReviewManager implements vscode.DecorationProvider { return this.commentsToCommentThreads(fileChange.comments); })); return comments.reduce((prev, curr) => prev.concat(curr), []); - } + }, + createNewCommentThread: this.createNewCommentThread.bind(this), + replyToCommentThread: this.replyToCommentThread.bind(this) }); } diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 87c533fd29f..2a45c11af46 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -993,13 +993,17 @@ export interface CommentThreadChangedEvent { export interface DocumentCommentProvider { - provideDocumentComments(model: model.ITextModel, token: CancellationToken): Promise; + provideDocumentComments(resource: URI, token: CancellationToken): Promise; + createNewCommentThread(resource: URI, range: Range, text: string, token: CancellationToken): Promise; + replyToCommentThread(resource: URI, range: Range, thread: CommentThread, text: string, token: CancellationToken): Promise; onDidChangeCommentThreads(): Event; } export interface WorkspaceCommentProvider { provideWorkspaceComments(token: CancellationToken): Promise; + createNewCommentThread(resource: URI, range: Range, text: string, token: CancellationToken): Promise; + replyToCommentThread(resource: URI, range: Range, thread: CommentThread, text: string, token: CancellationToken): Promise; onDidChangeCommentThreads(): Event; } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index a7e228b0233..472ce50a743 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5162,12 +5162,16 @@ declare namespace monaco.languages { } export interface DocumentCommentProvider { - provideDocumentComments(model: editor.ITextModel, token: CancellationToken): Promise; + provideDocumentComments(resource: Uri, token: CancellationToken): Promise; + createNewCommentThread(resource: Uri, range: Range, text: string, token: CancellationToken): Promise; + replyToCommentThread(resource: Uri, range: Range, thread: CommentThread, text: string, token: CancellationToken): Promise; onDidChangeCommentThreads(): IEvent; } export interface WorkspaceCommentProvider { provideWorkspaceComments(token: CancellationToken): Promise; + createNewCommentThread(resource: Uri, range: Range, text: string, token: CancellationToken): Promise; + replyToCommentThread(resource: Uri, range: Range, thread: CommentThread, text: string, token: CancellationToken): Promise; onDidChangeCommentThreads(): IEvent; } diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 60835577e74..6012813751e 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -430,7 +430,6 @@ declare module 'vscode' { interface CommentInfo { threads: CommentThread[]; commentingRanges?: Range[]; - postReviewComment?: Command; } export enum CommentThreadCollapsibleState { @@ -450,7 +449,6 @@ declare module 'vscode' { range: Range; comments: Comment[]; collapsibleState?: CommentThreadCollapsibleState; - postReviewComment?: Command; } interface Comment { @@ -479,11 +477,16 @@ declare module 'vscode' { interface DocumentCommentProvider { provideDocumentComments(document: TextDocument, token: CancellationToken): Promise; + createNewCommentThread?(document: TextDocument, range: Range, text: string, token: CancellationToken): Promise; + replyToCommentThread?(document: TextDocument, range: Range, commentThread: CommentThread, text: string, token: CancellationToken): Promise; onDidChangeCommentThreads?: Event; } interface WorkspaceCommentProvider { provideWorkspaceComments(token: CancellationToken): Promise; + createNewCommentThread?(document: TextDocument, range: Range, text: string, token: CancellationToken): Promise; + replyToCommentThread?(document: TextDocument, range: Range, commentThread: CommentThread, text: string, token: CancellationToken): Promise; + onDidChangeCommentThreads?: Event; } diff --git a/src/vs/workbench/api/electron-browser/mainThreadComments.ts b/src/vs/workbench/api/electron-browser/mainThreadComments.ts index 40782846b28..28ad0f96bf5 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadComments.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadComments.ts @@ -58,12 +58,21 @@ export class MainThreadComments extends Disposable implements MainThreadComments }); for (const handle of keys(this._documentProviders)) { - _commentService.registerDataProvider({ - provideDocumentComments: async (model, token) => { - return this._proxy.$provideDocumentComments(handle, model.uri); - }, - onDidChangeCommentThreads: null - }); + _commentService.registerDataProvider( + handle, + { + provideDocumentComments: async (uri, token) => { + return this._proxy.$provideDocumentComments(handle, uri); + }, + onDidChangeCommentThreads: null, + createNewCommentThread: async (uri, range, text, token) => { + return this._proxy.$createNewCommentThread(handle, uri, range, text); + }, + replyToCommentThread: async (uri, range, thread, text, token) => { + return this._proxy.$replyToCommentThread(handle, uri, range, thread, text); + } + } + ); } }); } diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 34aacf96eff..380f59aa37c 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -869,6 +869,8 @@ export interface ExtHostProgressShape { export interface ExtHostCommentsShape { $provideDocumentComments(handle: number, document: UriComponents): TPromise; + $createNewCommentThread?(handle: number, document: UriComponents, range: IRange, text: string): TPromise; + $replyToCommentThread?(handle: number, document: UriComponents, range: IRange, commentThread: modes.CommentThread, text: string): TPromise; $provideWorkspaceComments(handle: number): TPromise; } diff --git a/src/vs/workbench/api/node/extHostComments.ts b/src/vs/workbench/api/node/extHostComments.ts index c79205123b4..fcbfe20032f 100644 --- a/src/vs/workbench/api/node/extHostComments.ts +++ b/src/vs/workbench/api/node/extHostComments.ts @@ -14,6 +14,7 @@ import * as extHostTypeConverter from 'vs/workbench/api/node/extHostTypeConverte import * as vscode from 'vscode'; import { ExtHostCommentsShape, IMainContext, MainContext, MainThreadCommentsShape } from './extHost.protocol'; import { CommandsConverter } from './extHostCommands'; +import { IRange } from 'vs/editor/common/core/range'; export class ExtHostComments implements ExtHostCommentsShape { private static handlePool = 0; @@ -63,6 +64,34 @@ export class ExtHostComments implements ExtHostCommentsShape { }; } + $createNewCommentThread(handle: number, uri: UriComponents, range: IRange, text: string): TPromise { + const data = this._documents.getDocumentData(URI.revive(uri)); + const ran = extHostTypeConverter.toRange(range); + + if (!data || !data.document) { + return TPromise.as(null); + } + + return asWinJsPromise(token => { + let provider = this._documentProviders.get(handle); + return provider.createNewCommentThread(data.document, ran, text, token); + }).then(commentThread => commentThread ? convertToCommentThread(commentThread) : null); + } + + $replyToCommentThread(handle: number, uri: UriComponents, range: IRange, thread: modes.CommentThread, text: string): TPromise { + const data = this._documents.getDocumentData(URI.revive(uri)); + const ran = extHostTypeConverter.toRange(range); + + if (!data || !data.document) { + return TPromise.as(null); + } + + return asWinJsPromise(token => { + let provider = this._documentProviders.get(handle); + return provider.replyToCommentThread(data.document, ran, convertFromCommentThread(thread), text, token); + }).then(commentThread => commentThread ? convertToCommentThread(commentThread) : null); + } + $provideDocumentComments(handle: number, uri: UriComponents): TPromise { const data = this._documents.getDocumentData(URI.revive(uri)); if (!data || !data.document) { @@ -85,7 +114,7 @@ export class ExtHostComments implements ExtHostCommentsShape { return asWinJsPromise(token => { return provider.provideWorkspaceComments(token); }).then(comments => - comments.map(x => convertCommentThread(x, this._commandsConverter) + comments.map(x => convertToCommentThread(x) )); } @@ -94,9 +123,9 @@ export class ExtHostComments implements ExtHostCommentsShape { this._proxy.$onDidCommentThreadsChange(handle, { owner: handle, - changed: event.changed.map(x => convertCommentThread(x, this._commandsConverter)), - added: event.added.map(x => convertCommentThread(x, this._commandsConverter)), - removed: event.removed.map(x => convertCommentThread(x, this._commandsConverter)) + changed: event.changed.map(x => convertToCommentThread(x)), + added: event.added.map(x => convertToCommentThread(x)), + removed: event.removed.map(x => convertToCommentThread(x)) }); }); } @@ -105,24 +134,41 @@ export class ExtHostComments implements ExtHostCommentsShape { function convertCommentInfo(owner: number, vscodeCommentInfo: vscode.CommentInfo, commandsConverter: CommandsConverter): modes.CommentInfo { return { owner: owner, - threads: vscodeCommentInfo.threads.map(x => convertCommentThread(x, commandsConverter)), - commentingRanges: vscodeCommentInfo.commentingRanges ? vscodeCommentInfo.commentingRanges.map(range => extHostTypeConverter.fromRange(range)) : [], - reply: vscodeCommentInfo.postReviewComment ? commandsConverter.toInternal(vscodeCommentInfo.postReviewComment) : null + threads: vscodeCommentInfo.threads.map(x => convertToCommentThread(x)), + commentingRanges: vscodeCommentInfo.commentingRanges ? vscodeCommentInfo.commentingRanges.map(range => extHostTypeConverter.fromRange(range)) : [] }; } -function convertCommentThread(vscodeCommentThread: vscode.CommentThread, commandsConverter: CommandsConverter): modes.CommentThread { +function convertToCommentThread(vscodeCommentThread: vscode.CommentThread): modes.CommentThread { return { threadId: vscodeCommentThread.threadId, resource: vscodeCommentThread.resource.toString(), range: extHostTypeConverter.fromRange(vscodeCommentThread.range), - comments: vscodeCommentThread.comments.map(convertComment), - collapsibleState: vscodeCommentThread.collapsibleState, - reply: vscodeCommentThread.postReviewComment ? commandsConverter.toInternal(vscodeCommentThread.postReviewComment) : null + comments: vscodeCommentThread.comments.map(convertToComment), + collapsibleState: vscodeCommentThread.collapsibleState }; } -function convertComment(vscodeComment: vscode.Comment): modes.Comment { +function convertFromCommentThread(commentThread: modes.CommentThread): vscode.CommentThread { + return { + threadId: commentThread.threadId, + resource: URI.parse(commentThread.resource), + range: extHostTypeConverter.toRange(commentThread.range), + comments: commentThread.comments.map(convertFromComment), + collapsibleState: commentThread.collapsibleState + }; +} + +function convertFromComment(comment: modes.Comment): vscode.Comment { + return { + commentId: comment.commentId, + body: extHostTypeConverter.MarkdownString.to(comment.body), + userName: comment.userName, + gravatar: comment.gravatar + }; +} + +function convertToComment(vscodeComment: vscode.Comment): modes.Comment { return { commentId: vscodeComment.commentId, body: extHostTypeConverter.MarkdownString.from(vscodeComment.body), diff --git a/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts b/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts index 6f8db209aec..1c4f82ceaf3 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts @@ -31,6 +31,9 @@ import { transparent, editorForeground } from 'vs/platform/theme/common/colorReg import { IModeService } from 'vs/editor/common/services/modeService'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from '../../../../base/common/keyCodes'; +import { ICommentService } from '../../../services/comments/electron-browser/commentService'; +import { Range } from 'vs/editor/common/core/range'; + export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration'; const EXPAND_ACTION_CLASS = 'expand-review-action octicon octicon-chevron-down'; const COLLAPSE_ACTION_CLASS = 'expand-review-action octicon octicon-chevron-up'; @@ -45,7 +48,7 @@ export class CommentNode { public get domNode(): HTMLElement { return this._domNode; } - constructor(public comment: modes.Comment, ) { + constructor(public comment: modes.Comment) { this._domNode = $('div.review-comment').getHTMLElement(); this._domNode.tabIndex = 0; let avatar = $('div.avatar-container').appendTo(this._domNode).getHTMLElement(); @@ -56,7 +59,7 @@ export class CommentNode { let header = $('div').appendTo(commentDetailsContainer).getHTMLElement(); let author = $('strong.author').appendTo(header).getHTMLElement(); author.innerText = comment.userName; - this._body = $('comment-body').appendTo(commentDetailsContainer).getHTMLElement(); + this._body = $('div.comment-body').appendTo(commentDetailsContainer).getHTMLElement(); this._md = renderMarkdown(comment.body); this._body.appendChild(this._md); @@ -126,7 +129,8 @@ export class ReviewZoneWidget extends ZoneWidget { replyCommand: modes.Command, options: IOptions = {}, private readonly themeService: IThemeService, - private readonly commandService: ICommandService + private readonly commandService: ICommandService, + private readonly commentService: ICommentService ) { super(editor, options); this._resizeObserver = null; @@ -289,6 +293,8 @@ export class ReviewZoneWidget extends ZoneWidget { this._commentThread = commentThread; this._commentElements = newCommentNodeList; + let secondaryHeading = this._commentThread.comments.filter(arrays.uniqueFilter(comment => comment.userName)).map(comment => `@${comment.userName}`).join(', '); + $(this._secondaryHeading).safeInnerHtml(secondaryHeading); } protected _doLayout(heightInPixel: number, widthInPixel: number): void { @@ -335,72 +341,81 @@ export class ReviewZoneWidget extends ZoneWidget { this._commentsElement.appendChild(newCommentNode.domNode); } - if (this._commentThread.reply) { - const hasExistingComments = this._commentThread.comments.length > 0; - const commentForm = $('.comment-form').appendTo(this._bodyElement).getHTMLElement(); - this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, commentForm, SimpleCommentEditor.getEditorOptions()); - const modeId = hasExistingComments ? this._commentThread.threadId : ++INMEM_MODEL_ID; - const resource = URI.parse(`${COMMENT_SCHEME}:commentinput-${modeId}.md`); - const model = this.modelService.createModel('', this.modeService.getOrCreateModeByFilenameOrFirstLine(resource.path), resource, true); - this._localToDispose.push(model); - this._commentEditor.setModel(model); - this._localToDispose.push(this._commentEditor); - this._localToDispose.push(this._commentEditor.getModel().onDidChangeContent(() => this.setCommentEditorDecorations())); - this.setCommentEditorDecorations(); + const hasExistingComments = this._commentThread.comments.length > 0; + const commentForm = $('.comment-form').appendTo(this._bodyElement).getHTMLElement(); + this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, commentForm, SimpleCommentEditor.getEditorOptions()); + const modeId = hasExistingComments ? this._commentThread.threadId : ++INMEM_MODEL_ID; + const resource = URI.parse(`${COMMENT_SCHEME}:commentinput-${modeId}.md`); + const model = this.modelService.createModel('', this.modeService.getOrCreateModeByFilenameOrFirstLine(resource.path), resource, true); + this._localToDispose.push(model); + this._commentEditor.setModel(model); + this._localToDispose.push(this._commentEditor); + this._localToDispose.push(this._commentEditor.getModel().onDidChangeContent(() => this.setCommentEditorDecorations())); + this.setCommentEditorDecorations(); - // Only add the additional step of clicking a reply button to expand the textarea when there are existing comments - if (hasExistingComments) { - const reviewThreadReplyButton = $('button.review-thread-reply-button').appendTo(commentForm).getHTMLElement(); - reviewThreadReplyButton.title = 'Reply...'; - reviewThreadReplyButton.textContent = 'Reply...'; - // bind click/escape actions for reviewThreadReplyButton and textArea - reviewThreadReplyButton.onclick = () => { - if (!dom.hasClass(commentForm, 'expand')) { - dom.addClass(commentForm, 'expand'); - this._commentEditor.focus(); - } - }; - - this._commentEditor.onDidBlurEditor(() => { - if (this._commentEditor.getModel().getValueLength() === 0 && dom.hasClass(commentForm, 'expand')) { - dom.removeClass(commentForm, 'expand'); - } - }); - } else { - dom.addClass(commentForm, 'expand'); - } - - this._localToDispose.push(this._commentEditor.onKeyDown((ev: IKeyboardEvent) => { - const hasExistingComments = this._commentThread.comments.length > 0; - if (this._commentEditor.getModel().getValueLength() === 0 && ev.keyCode === KeyCode.Escape && hasExistingComments) { - if (dom.hasClass(commentForm, 'expand')) { - dom.removeClass(commentForm, 'expand'); - } + // Only add the additional step of clicking a reply button to expand the textarea when there are existing comments + if (hasExistingComments) { + const reviewThreadReplyButton = $('button.review-thread-reply-button').appendTo(commentForm).getHTMLElement(); + reviewThreadReplyButton.title = 'Reply...'; + reviewThreadReplyButton.textContent = 'Reply...'; + // bind click/escape actions for reviewThreadReplyButton and textArea + reviewThreadReplyButton.onclick = () => { + if (!dom.hasClass(commentForm, 'expand')) { + dom.addClass(commentForm, 'expand'); + this._commentEditor.focus(); } - })); + }; - const formActions = $('.form-actions').appendTo(commentForm).getHTMLElement(); - - const button = new Button(formActions); - attachButtonStyler(button, this.themeService); - button.label = this.commentThread.reply.title; - button.onDidClick(async () => { - let newComment = await this.commandService.executeCommand(this._replyCommand.id, this.editor.getModel().uri, { - start: { line: lineNumber, column: 1 }, - end: { line: lineNumber, column: 1 } - }, this._commentThread, this._commentEditor.getValue()); - if (newComment) { - this._commentEditor.setValue(''); - this._commentThread.comments.push(newComment); - let newCommentNode = new CommentNode(newComment); - this._commentElements.push(newCommentNode); - this._commentsElement.appendChild(newCommentNode.domNode); - let secondaryHeading = this._commentThread.comments.filter(arrays.uniqueFilter(comment => comment.userName)).map(comment => `@${comment.userName}`).join(', '); - $(this._secondaryHeading).safeInnerHtml(secondaryHeading); + this._commentEditor.onDidBlurEditor(() => { + if (this._commentEditor.getModel().getValueLength() === 0 && dom.hasClass(commentForm, 'expand')) { + dom.removeClass(commentForm, 'expand'); } }); + } else { + dom.addClass(commentForm, 'expand'); } + this._localToDispose.push(this._commentEditor.onKeyDown((ev: IKeyboardEvent) => { + const hasExistingComments = this._commentThread.comments.length > 0; + if (this._commentEditor.getModel().getValueLength() === 0 && ev.keyCode === KeyCode.Escape && hasExistingComments) { + if (dom.hasClass(commentForm, 'expand')) { + dom.removeClass(commentForm, 'expand'); + } + } + })); + + const formActions = $('.form-actions').appendTo(commentForm).getHTMLElement(); + + const button = new Button(formActions); + attachButtonStyler(button, this.themeService); + button.label = 'Add comment'; + button.onDidClick(async () => { + let newCommentThread; + if (this._commentThread.threadId) { + // reply + newCommentThread = await this.commentService.replyToCommentThread( + this._owner, + this.editor.getModel().uri, + new Range(lineNumber, 1, lineNumber, 1), + this._commentThread, + this._commentEditor.getValue() + ); + } else { + newCommentThread = await this.commentService.createNewCommenThread( + this._owner, + this.editor.getModel().uri, + new Range(lineNumber, 1, lineNumber, 1), + this._commentEditor.getValue() + ); + } + + this._commentEditor.setValue(''); + if (newCommentThread) { + this.update(newCommentThread); + } + }); + + this._resizeObserver = new ResizeObserver(entries => { if (entries[0].target === this._bodyElement && !this._isCollapsed) { const lineHeight = this.editor.getConfiguration().lineHeight; diff --git a/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts b/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts index 11985caeba4..796b974d4fb 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentsEditorContribution.ts @@ -119,7 +119,7 @@ export class ReviewController implements IEditorContribution { this._commentInfos.forEach(info => { info.threads.forEach(thread => { - let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.editor, info.owner, thread, info.reply, {}, this.themeService, this.commandService); + let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.editor, info.owner, thread, info.reply, {}, this.themeService, this.commandService, this.commentService); zoneWidget.display(thread.range.startLineNumber); this._commentWidgets.push(zoneWidget); }); @@ -214,7 +214,7 @@ export class ReviewController implements IEditorContribution { } }); added.forEach(thread => { - let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.editor, e.owner, thread, thread.reply, {}, this.themeService, this.commandService); + let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.editor, e.owner, thread, thread.reply, {}, this.themeService, this.commandService, this.commentService); zoneWidget.display(thread.range.startLineNumber); this._commentWidgets.push(zoneWidget); this._commentInfos.filter(info => info.owner === e.owner)[0].threads.push(thread); @@ -243,7 +243,7 @@ export class ReviewController implements IEditorContribution { }, reply: replyCommand, collapsibleState: CommentThreadCollapsibleState.Expanded, - }, replyCommand, {}, this.themeService, this.commandService); + }, replyCommand, {}, this.themeService, this.commandService, this.commentService); this._newCommentWidget.onDidClose(e => { this._newCommentWidget = null; @@ -356,7 +356,7 @@ export class ReviewController implements IEditorContribution { this._commentInfos.forEach(info => { info.threads.forEach(thread => { - let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.editor, info.owner, thread, info.reply, {}, this.themeService, this.commandService); + let zoneWidget = new ReviewZoneWidget(this.instantiationService, this.modeService, this.modelService, this.editor, info.owner, thread, info.reply, {}, this.themeService, this.commandService, this.commentService); zoneWidget.display(thread.range.startLineNumber); this._commentWidgets.push(zoneWidget); }); diff --git a/src/vs/workbench/services/comments/electron-browser/commentService.ts b/src/vs/workbench/services/comments/electron-browser/commentService.ts index 8e1c5e1af47..8f9f09bc144 100644 --- a/src/vs/workbench/services/comments/electron-browser/commentService.ts +++ b/src/vs/workbench/services/comments/electron-browser/commentService.ts @@ -10,6 +10,9 @@ 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 { Range } from 'vs/editor/common/core/range'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { asWinJsPromise } from 'vs/base/common/async'; export const ICommentService = createDecorator('commentService'); @@ -26,8 +29,10 @@ export interface ICommentService { setComments(resource: URI, commentInfos: CommentInfo[]): void; setAllComments(commentsByResource: CommentThread[]): void; removeAllComments(): void; - registerDataProvider(commentProvider: DocumentCommentProvider | WorkspaceCommentProvider): void; + registerDataProvider(owner: number, commentProvider: DocumentCommentProvider | WorkspaceCommentProvider): void; updateComments(event: CommentThreadChangedEvent): void; + createNewCommenThread(owner: number, resource: URI, range: Range, text: string): TPromise; + replyToCommentThread(owner: number, resource: URI, range: Range, thread: CommentThread, text: string): TPromise; } export class CommentService extends Disposable implements ICommentService { @@ -42,8 +47,7 @@ export class CommentService extends Disposable implements ICommentService { private readonly _onDidUpdateCommentThreads: Emitter = this._register(new Emitter()); readonly onDidUpdateCommentThreads: Event = this._onDidUpdateCommentThreads.event; - private _commentProviders: (DocumentCommentProvider | WorkspaceCommentProvider)[] = []; - + private _commentProviders = new Map(); constructor() { super(); } @@ -60,12 +64,29 @@ export class CommentService extends Disposable implements ICommentService { this._onDidSetAllCommentThreads.fire([]); } - registerDataProvider(commentProvider: DocumentCommentProvider | WorkspaceCommentProvider) { - // @todo use map - this._commentProviders.push(commentProvider); + registerDataProvider(owner: number, commentProvider: DocumentCommentProvider | WorkspaceCommentProvider) { + this._commentProviders.set(owner, commentProvider); } updateComments(event: CommentThreadChangedEvent): void { this._onDidUpdateCommentThreads.fire(event); } + + createNewCommenThread(owner: number, resource: URI, range: Range, text: string): TPromise { + let commentProvider = this._commentProviders.get(owner); + + if (commentProvider) { + return asWinJsPromise(token => commentProvider.createNewCommentThread(resource, range, text, token)); + } + return null; + } + + replyToCommentThread(owner: number, resource: URI, range: Range, thread: CommentThread, text: string): TPromise { + let commentProvider = this._commentProviders.get(owner); + + if (commentProvider) { + return asWinJsPromise(token => commentProvider.replyToCommentThread(resource, range, thread, text, token)); + } + return null; + } }