diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index de183d00609f19b181b2a4f05794c5ff138e743a..a702d0280d7bba6103451aba30f507f5a8013404 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1008,6 +1008,7 @@ export interface Comment { readonly body: IMarkdownString; readonly userName: string; readonly gravatar: string; + readonly canEdit?: boolean; readonly command?: Command; } @@ -1039,6 +1040,7 @@ export interface DocumentCommentProvider { 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; + editComment(resource: URI, comment: Comment, text: string, token: CancellationToken): Promise; onDidChangeCommentThreads(): Event; } @@ -1047,8 +1049,6 @@ export interface DocumentCommentProvider { */ 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/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index d7a25cd24882cea69d21cf9c6e390d26441a421f..bddbe9f45dc903eb18f2f6b3d147e4e61c59dc42 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -656,10 +656,37 @@ declare module 'vscode' { } interface Comment { + /** + * The id of the comment + */ commentId: string; + + /** + * The text of the comment + */ body: MarkdownString; + + /** + * The display name of the user who created the comment + */ userName: string; + + /** + * The avatar src of the user who created the comment + */ gravatar: string; + + /** + * Whether the current user has permission to edit the comment. + * + * This will be treated as false if the comment is provided by a `WorkspaceCommentProvider`, or + * if it is provided by a `DocumentCommentProvider` and no `editComment` method is given. + */ + canEdit?: boolean; + + /** + * The command to be executed if the comment is selected in the Comments Panel + */ command?: Command; } @@ -681,14 +708,42 @@ declare module 'vscode' { } interface DocumentCommentProvider { + /** + * Provide the commenting ranges and comment threads for the given document. The comments are displayed within the editor. + */ provideDocumentComments(document: TextDocument, token: CancellationToken): Promise; + + /** + * Called when a user adds a new comment thread in the document at the specified range, with body text. + */ createNewCommentThread(document: TextDocument, range: Range, text: string, token: CancellationToken): Promise; + + /** + * Called when a user replies to a new comment thread in the document at the specified range, with body text. + */ replyToCommentThread(document: TextDocument, range: Range, commentThread: CommentThread, text: string, token: CancellationToken): Promise; + + /** + * Called when a user edits the comment body to the be new text text. + */ + editComment?(document: TextDocument, comment: Comment, text: string, token: CancellationToken): Promise; + + /** + * Notify of updates to comment threads. + */ onDidChangeCommentThreads: Event; } interface WorkspaceCommentProvider { + /** + * Provide all comments for the workspace. Comments are shown within the comments panel. Selecting a comment + * from the panel runs the comment's command. + */ provideWorkspaceComments(token: CancellationToken): Promise; + + /** + * Notify of updates to comment threads. + */ onDidChangeCommentThreads: Event; } diff --git a/src/vs/workbench/api/electron-browser/mainThreadComments.ts b/src/vs/workbench/api/electron-browser/mainThreadComments.ts index f1c5434edd9a0ce7b41af0106b8f4e57db8c576e..07e70cbc2ed5eeb320267616eb44a9b6c06c848e 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadComments.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadComments.ts @@ -77,6 +77,9 @@ export class MainThreadComments extends Disposable implements MainThreadComments }, replyToCommentThread: async (uri, range, thread, text, token) => { return this._proxy.$replyToCommentThread(handle, uri, range, thread, text); + }, + editComment: async (uri, comment, text, token) => { + return this._proxy.$editComment(handle, uri, comment, text); } } ); diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 6b9961f7e783cb381cacf522eeffba5b7bfd51cd..6b04f28b0dba37585b574d535d2f460cb9053c7c 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -995,6 +995,7 @@ export interface ExtHostCommentsShape { $provideDocumentComments(handle: number, document: UriComponents): Thenable; $createNewCommentThread(handle: number, document: UriComponents, range: IRange, text: string): Thenable; $replyToCommentThread(handle: number, document: UriComponents, range: IRange, commentThread: modes.CommentThread, text: string): Thenable; + $editComment(handle: number, document: UriComponents, comment: modes.Comment, text: string): Thenable; $provideWorkspaceComments(handle: number): Thenable; } diff --git a/src/vs/workbench/api/node/extHostComments.ts b/src/vs/workbench/api/node/extHostComments.ts index c981115c0a6e007182b6578334afa8c6175f668a..772e0878504774457a50bd16415f4e2d5b52dbfb 100644 --- a/src/vs/workbench/api/node/extHostComments.ts +++ b/src/vs/workbench/api/node/extHostComments.ts @@ -73,10 +73,10 @@ export class ExtHostComments implements ExtHostCommentsShape { return TPromise.as(null); } + const provider = this._documentProviders.get(handle); return asThenable(() => { - let provider = this._documentProviders.get(handle); return provider.createNewCommentThread(data.document, ran, text, CancellationToken.None); - }).then(commentThread => commentThread ? convertToCommentThread(commentThread, this._commandsConverter) : null); + }).then(commentThread => commentThread ? convertToCommentThread(provider, commentThread, this._commandsConverter) : null); } $replyToCommentThread(handle: number, uri: UriComponents, range: IRange, thread: modes.CommentThread, text: string): Thenable { @@ -87,10 +87,23 @@ export class ExtHostComments implements ExtHostCommentsShape { return TPromise.as(null); } + const provider = this._documentProviders.get(handle); return asThenable(() => { - let provider = this._documentProviders.get(handle); return provider.replyToCommentThread(data.document, ran, convertFromCommentThread(thread), text, CancellationToken.None); - }).then(commentThread => commentThread ? convertToCommentThread(commentThread, this._commandsConverter) : null); + }).then(commentThread => commentThread ? convertToCommentThread(provider, commentThread, this._commandsConverter) : null); + } + + $editComment(handle: number, uri: UriComponents, comment: modes.Comment, text: string): Thenable { + const data = this._documents.getDocumentData(URI.revive(uri)); + + if (!data || !data.document) { + throw new Error('Unable to retrieve document from URI'); + } + + const provider = this._documentProviders.get(handle); + return asThenable(() => { + return provider.editComment(data.document, convertFromComment(comment), text, CancellationToken.None); + }).then(comment => convertToComment(provider, comment, this._commandsConverter)); } $provideDocumentComments(handle: number, uri: UriComponents): Thenable { @@ -99,11 +112,10 @@ export class ExtHostComments implements ExtHostCommentsShape { return TPromise.as(null); } + const provider = this._documentProviders.get(handle); return asThenable(() => { - let provider = this._documentProviders.get(handle); return provider.provideDocumentComments(data.document, CancellationToken.None); - }) - .then(commentInfo => commentInfo ? convertCommentInfo(handle, commentInfo, this._commandsConverter) : null); + }).then(commentInfo => commentInfo ? convertCommentInfo(handle, provider, commentInfo, this._commandsConverter) : null); } $provideWorkspaceComments(handle: number): Thenable { @@ -115,7 +127,7 @@ export class ExtHostComments implements ExtHostCommentsShape { return asThenable(() => { return provider.provideWorkspaceComments(CancellationToken.None); }).then(comments => - comments.map(x => convertToCommentThread(x, this._commandsConverter) + comments.map(comment => convertToCommentThread(provider, comment, this._commandsConverter) )); } @@ -124,28 +136,28 @@ export class ExtHostComments implements ExtHostCommentsShape { this._proxy.$onDidCommentThreadsChange(handle, { owner: handle, - changed: event.changed.map(x => convertToCommentThread(x, this._commandsConverter)), - added: event.added.map(x => convertToCommentThread(x, this._commandsConverter)), - removed: event.removed.map(x => convertToCommentThread(x, this._commandsConverter)) + changed: event.changed.map(thread => convertToCommentThread(provider, thread, this._commandsConverter)), + added: event.added.map(thread => convertToCommentThread(provider, thread, this._commandsConverter)), + removed: event.removed.map(thread => convertToCommentThread(provider, thread, this._commandsConverter)) }); }); } } -function convertCommentInfo(owner: number, vscodeCommentInfo: vscode.CommentInfo, commandsConverter: CommandsConverter): modes.CommentInfo { +function convertCommentInfo(owner: number, provider: vscode.DocumentCommentProvider, vscodeCommentInfo: vscode.CommentInfo, commandsConverter: CommandsConverter): modes.CommentInfo { return { owner: owner, - threads: vscodeCommentInfo.threads.map(x => convertToCommentThread(x, commandsConverter)), + threads: vscodeCommentInfo.threads.map(x => convertToCommentThread(provider, x, commandsConverter)), commentingRanges: vscodeCommentInfo.commentingRanges ? vscodeCommentInfo.commentingRanges.map(range => extHostTypeConverter.Range.from(range)) : [] }; } -function convertToCommentThread(vscodeCommentThread: vscode.CommentThread, commandsConverter: CommandsConverter): modes.CommentThread { +function convertToCommentThread(provider: vscode.DocumentCommentProvider | vscode.WorkspaceCommentProvider, vscodeCommentThread: vscode.CommentThread, commandsConverter: CommandsConverter): modes.CommentThread { return { threadId: vscodeCommentThread.threadId, resource: vscodeCommentThread.resource.toString(), range: extHostTypeConverter.Range.from(vscodeCommentThread.range), - comments: vscodeCommentThread.comments.map(comment => convertToComment(comment, commandsConverter)), + comments: vscodeCommentThread.comments.map(comment => convertToComment(provider, comment, commandsConverter)), collapsibleState: vscodeCommentThread.collapsibleState }; } @@ -165,16 +177,19 @@ function convertFromComment(comment: modes.Comment): vscode.Comment { commentId: comment.commentId, body: extHostTypeConverter.MarkdownString.to(comment.body), userName: comment.userName, - gravatar: comment.gravatar + gravatar: comment.gravatar, + canEdit: comment.canEdit }; } -function convertToComment(vscodeComment: vscode.Comment, commandsConverter: CommandsConverter): modes.Comment { +function convertToComment(provider: vscode.DocumentCommentProvider | vscode.WorkspaceCommentProvider, vscodeComment: vscode.Comment, commandsConverter: CommandsConverter): modes.Comment { + const canEdit = !!(provider as vscode.DocumentCommentProvider).editComment && vscodeComment.canEdit; return { commentId: vscodeComment.commentId, body: extHostTypeConverter.MarkdownString.from(vscodeComment.body), userName: vscodeComment.userName, gravatar: vscodeComment.gravatar, + canEdit: canEdit, command: vscodeComment.command ? commandsConverter.toInternal(vscodeComment.command) : null }; } diff --git a/src/vs/workbench/parts/comments/electron-browser/commentNode.ts b/src/vs/workbench/parts/comments/electron-browser/commentNode.ts new file mode 100644 index 0000000000000000000000000000000000000000..ce207ac1ed62770e23380b5dec11c7e8bc8d3b1f --- /dev/null +++ b/src/vs/workbench/parts/comments/electron-browser/commentNode.ts @@ -0,0 +1,244 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as nls from 'vs/nls'; +import * as dom from 'vs/base/browser/dom'; +import * as modes from 'vs/editor/common/modes'; +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { Action } from 'vs/base/common/actions'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { ITextModel } from 'vs/editor/common/model'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { inputValidationErrorBorder } from 'vs/platform/theme/common/colorRegistry'; +import { attachButtonStyler } from 'vs/platform/theme/common/styler'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ICommentService } from 'vs/workbench/parts/comments/electron-browser/commentService'; +import { SimpleCommentEditor } from 'vs/workbench/parts/comments/electron-browser/simpleCommentEditor'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { isMacintosh } from 'vs/base/common/platform'; +import { Selection } from 'vs/editor/common/core/selection'; + +const UPDATE_COMMENT_LABEL = nls.localize('label.updateComment', "Update comment"); +const UPDATE_IN_PROGRESS_LABEL = nls.localize('label.updatingComment', "Updating comment..."); + +export class CommentNode extends Disposable { + private _domNode: HTMLElement; + private _body: HTMLElement; + private _md: HTMLElement; + private _clearTimeout: any; + + private _editAction: Action; + private _commentEditContainer: HTMLElement; + private _commentEditor: SimpleCommentEditor; + private _commentEditorModel: ITextModel; + private _updateCommentButton: Button; + private _errorEditingContainer: HTMLElement; + + public get domNode(): HTMLElement { + return this._domNode; + } + + constructor( + public comment: modes.Comment, + private owner: number, + private resource: URI, + private markdownRenderer: MarkdownRenderer, + private themeService: IThemeService, + private instantiationService: IInstantiationService, + private commentService: ICommentService, + private modelService: IModelService, + private modeService: IModeService + ) { + super(); + + this._domNode = dom.$('div.review-comment'); + this._domNode.tabIndex = 0; + const avatar = dom.append(this._domNode, dom.$('div.avatar-container')); + const img = dom.append(avatar, dom.$('img.avatar')); + img.src = comment.gravatar; + const commentDetailsContainer = dom.append(this._domNode, dom.$('.review-comment-contents')); + + this.createHeader(commentDetailsContainer); + + this._body = dom.append(commentDetailsContainer, dom.$('div.comment-body')); + this._md = this.markdownRenderer.render(comment.body).element; + this._body.appendChild(this._md); + + this._domNode.setAttribute('aria-label', `${comment.userName}, ${comment.body.value}`); + this._domNode.setAttribute('role', 'treeitem'); + this._clearTimeout = null; + } + + private createHeader(commentDetailsContainer: HTMLElement): void { + const header = dom.append(commentDetailsContainer, dom.$('div.comment-title')); + const author = dom.append(header, dom.$('strong.author')); + author.innerText = this.comment.userName; + + const actions: Action[] = []; + if (this.comment.canEdit) { + this._editAction = this.createEditAction(commentDetailsContainer); + actions.push(this._editAction); + } + + if (actions.length) { + const actionsContainer = dom.append(header, dom.$('.comment-actions.hidden')); + const actionBar = new ActionBar(actionsContainer, {}); + this._toDispose.push(actionBar); + this.registerActionBarListeners(actionsContainer); + + actions.forEach(action => actionBar.push(action, { label: false, icon: true })); + } + } + + private createCommentEditor(): void { + const container = dom.append(this._commentEditContainer, dom.$('.edit-textarea')); + this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, container, SimpleCommentEditor.getEditorOptions()); + const resource = URI.parse(`comment:commentinput-${this.comment.commentId}-${Date.now()}.md`); + this._commentEditorModel = this.modelService.createModel('', this.modeService.getOrCreateModeByFilenameOrFirstLine(resource.path), resource, true); + + this._commentEditor.setModel(this._commentEditorModel); + this._commentEditor.setValue(this.comment.body.value); + this._commentEditor.layout({ width: container.clientWidth - 14, height: 90 }); + this._commentEditor.focus(); + const lastLine = this._commentEditorModel.getLineCount(); + const lastColumn = this._commentEditorModel.getLineContent(lastLine).length + 1; + this._commentEditor.setSelection(new Selection(lastLine, lastColumn, lastLine, lastColumn)); + + this._toDispose.push(this._commentEditor.onKeyDown((e: IKeyboardEvent) => { + const isCmdOrCtrl = isMacintosh ? e.metaKey : e.ctrlKey; + if (this._updateCommentButton.enabled && e.keyCode === KeyCode.Enter && isCmdOrCtrl) { + this.editComment(); + } + })); + + this._toDispose.push(this._commentEditor); + this._toDispose.push(this._commentEditorModel); + } + + private removeCommentEditor() { + this._editAction.enabled = true; + this._body.classList.remove('hidden'); + + this._commentEditorModel.dispose(); + this._commentEditor.dispose(); + this._commentEditor = null; + + this._commentEditContainer.remove(); + } + + private editComment(): void { + this._updateCommentButton.enabled = false; + this._updateCommentButton.label = UPDATE_IN_PROGRESS_LABEL; + + try { + this.commentService.editComment(this.owner, this.resource, this.comment, this._commentEditor.getValue()).then(editedComment => { + this._updateCommentButton.enabled = true; + this._updateCommentButton.label = UPDATE_COMMENT_LABEL; + this._commentEditor.getDomNode().style.outline = ''; + this.removeCommentEditor(); + this.update(editedComment); + }); + } catch (e) { + this._updateCommentButton.enabled = true; + this._updateCommentButton.label = UPDATE_COMMENT_LABEL; + + this._commentEditor.getDomNode().style.outline = `1px solid ${this.themeService.getTheme().getColor(inputValidationErrorBorder)}`; + this._errorEditingContainer.textContent = nls.localize('commentCreationError', "Updating the comment failed: {0}.", e.message); + this._errorEditingContainer.classList.remove('hidden'); + this._commentEditor.focus(); + } + } + + private createEditAction(commentDetailsContainer: HTMLElement): Action { + return new Action('comment.edit', nls.localize('label.edit', "Edit"), 'octicon octicon-pencil', true, () => { + this._body.classList.add('hidden'); + this._commentEditContainer = dom.append(commentDetailsContainer, dom.$('.edit-container')); + this.createCommentEditor(); + + this._errorEditingContainer = dom.append(this._commentEditContainer, dom.$('.validation-error.hidden')); + const formActions = dom.append(this._commentEditContainer, dom.$('.form-actions')); + + const cancelEditButton = new Button(formActions); + cancelEditButton.label = nls.localize('label.cancel', "Cancel"); + attachButtonStyler(cancelEditButton, this.themeService); + + this._toDispose.push(cancelEditButton.onDidClick(_ => { + this.removeCommentEditor(); + })); + + this._updateCommentButton = new Button(formActions); + this._updateCommentButton.label = UPDATE_COMMENT_LABEL; + attachButtonStyler(this._updateCommentButton, this.themeService); + + this._toDispose.push(this._updateCommentButton.onDidClick(_ => { + this.editComment(); + })); + + this._toDispose.push(this._commentEditor.onDidChangeModelContent(_ => { + this._updateCommentButton.enabled = !!this._commentEditor.getValue(); + })); + + this._editAction.enabled = false; + return null; + }); + } + + private registerActionBarListeners(actionsContainer: HTMLElement): void { + this._toDispose.push(dom.addDisposableListener(this._domNode, 'mouseenter', () => { + actionsContainer.classList.remove('hidden'); + })); + + this._toDispose.push(dom.addDisposableListener(this._domNode, 'focus', () => { + actionsContainer.classList.remove('hidden'); + })); + + this._toDispose.push(dom.addDisposableListener(this._domNode, 'mouseleave', (e: MouseEvent) => { + if (!this._domNode.contains(document.activeElement)) { + actionsContainer.classList.add('hidden'); + } + })); + + this._toDispose.push(dom.addDisposableListener(this._domNode, 'focusout', (e: FocusEvent) => { + if (!this._domNode.contains((e.relatedTarget))) { + actionsContainer.classList.add('hidden'); + + if (this._commentEditor && this._commentEditor.getValue() === this.comment.body.value) { + this.removeCommentEditor(); + } + } + })); + } + + update(newComment: modes.Comment) { + if (newComment.body !== this.comment.body) { + this._body.removeChild(this._md); + this._md = this.markdownRenderer.render(newComment.body).element; + this._body.appendChild(this._md); + } + + this.comment = newComment; + } + + focus() { + this.domNode.focus(); + if (!this._clearTimeout) { + dom.addClass(this.domNode, 'focus'); + this._clearTimeout = setTimeout(() => { + dom.removeClass(this.domNode, 'focus'); + }, 3000); + } + } + + dispose() { + this._toDispose.forEach(disposeable => disposeable.dispose()); + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/comments/electron-browser/commentService.ts b/src/vs/workbench/parts/comments/electron-browser/commentService.ts index c458cce2bd896d1bd2865773734dbaee6d3b2e73..119a1fb5c8886410eaf3515f565f21bcd260297f 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentService.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentService.ts @@ -5,7 +5,7 @@ 'use strict'; -import { CommentThread, DocumentCommentProvider, CommentThreadChangedEvent, CommentInfo } from 'vs/editor/common/modes'; +import { CommentThread, DocumentCommentProvider, CommentThreadChangedEvent, CommentInfo, 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'; @@ -41,6 +41,7 @@ export interface ICommentService { updateComments(event: CommentThreadChangedEvent): void; createNewCommentThread(owner: number, resource: URI, range: Range, text: string): Promise; replyToCommentThread(owner: number, resource: URI, range: Range, thread: CommentThread, text: string): Promise; + editComment(owner: number, resource: URI, comment: Comment, text: string): Promise; getComments(resource: URI): Promise; } @@ -114,6 +115,16 @@ export class CommentService extends Disposable implements ICommentService { return null; } + editComment(owner: number, resource: URI, comment: Comment, text: string): Promise { + const commentProvider = this._commentProviders.get(owner); + + if (commentProvider) { + return commentProvider.editComment(resource, comment, text, CancellationToken.None); + } + + return null; + } + getComments(resource: URI): Promise { const result = []; for (const handle of keys(this._commentProviders)) { diff --git a/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts b/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts index eebb171da248e3e7d8acc536b2c84c14ec0d64fa..cc5017cc34c3d12e8d75be80aced2e6f76ada99c 100644 --- a/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts +++ b/src/vs/workbench/parts/comments/electron-browser/commentThreadWidget.ts @@ -37,65 +37,12 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer'; import { IMarginData } from 'vs/editor/browser/controller/mouseTarget'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { CommentNode } from 'vs/workbench/parts/comments/electron-browser/commentNode'; export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration'; const COLLAPSE_ACTION_CLASS = 'expand-review-action octicon octicon-x'; const COMMENT_SCHEME = 'comment'; -export class CommentNode { - private _domNode: HTMLElement; - private _body: HTMLElement; - private _md: HTMLElement; - private _clearTimeout: any; - - public get domNode(): HTMLElement { - return this._domNode; - } - - constructor( - public comment: modes.Comment, - private markdownRenderer: MarkdownRenderer, - ) { - this._domNode = dom.$('div.review-comment'); - this._domNode.tabIndex = 0; - let avatar = dom.append(this._domNode, dom.$('div.avatar-container')); - let img = dom.append(avatar, dom.$('img.avatar')); - img.src = comment.gravatar; - let commentDetailsContainer = dom.append(this._domNode, dom.$('.review-comment-contents')); - - let header = dom.append(commentDetailsContainer, dom.$('div')); - let author = dom.append(header, dom.$('strong.author')); - author.innerText = comment.userName; - this._body = dom.append(commentDetailsContainer, dom.$('div.comment-body')); - this._md = this.markdownRenderer.render(comment.body).element; - this._body.appendChild(this._md); - - this._domNode.setAttribute('aria-label', `${comment.userName}, ${comment.body.value}`); - this._domNode.setAttribute('role', 'treeitem'); - this._clearTimeout = null; - } - - update(newComment: modes.Comment) { - if (newComment.body !== this.comment.body) { - this._body.removeChild(this._md); - this._md = this.markdownRenderer.render(newComment.body).element; - this._body.appendChild(this._md); - } - - this.comment = newComment; - } - - focus() { - this.domNode.focus(); - if (!this._clearTimeout) { - dom.addClass(this.domNode, 'focus'); - this._clearTimeout = setTimeout(() => { - dom.removeClass(this.domNode, 'focus'); - }, 3000); - } - } -} - let INMEM_MODEL_ID = 0; export class ReviewZoneWidget extends ZoneWidget { @@ -219,9 +166,9 @@ export class ReviewZoneWidget extends ZoneWidget { this.dispose(); return null; } + this._isCollapsed = true; this.hide(); - return null; }); @@ -265,7 +212,18 @@ export class ReviewZoneWidget extends ZoneWidget { lastCommentElement = oldCommentNode[0].domNode; newCommentNodeList.unshift(oldCommentNode[0]); } else { - let newElement = new CommentNode(currentComment, this._markdownRenderer); + let newElement = new CommentNode( + currentComment, + this.owner, + this.editor.getModel().uri, + this._markdownRenderer, + this.themeService, + this.instantiationService, + this.commentService, + this.modelService, + this.modeService); + this._disposables.push(newElement); + newCommentNodeList.unshift(newElement); if (lastCommentElement) { this._commentsElement.insertBefore(newElement.domNode, lastCommentElement); @@ -302,7 +260,16 @@ export class ReviewZoneWidget extends ZoneWidget { this._commentElements = []; for (let i = 0; i < this._commentThread.comments.length; i++) { - let newCommentNode = new CommentNode(this._commentThread.comments[i], this._markdownRenderer); + let newCommentNode = new CommentNode(this._commentThread.comments[i], + this.owner, + this.editor.getModel().uri, + this._markdownRenderer, + this.themeService, + this.instantiationService, + this.commentService, + this.modelService, + this.modeService); + this._disposables.push(newCommentNode); this._commentElements.push(newCommentNode); this._commentsElement.appendChild(newCommentNode.domNode); } @@ -592,18 +559,18 @@ export class ReviewZoneWidget extends ZoneWidget { const content: string[] = []; const linkColor = theme.getColor(textLinkForeground); if (linkColor) { - content.push(`.monaco-editor .review-widget .body .review-comment a { color: ${linkColor} }`); + content.push(`.monaco-editor .review-widget .body .comment-body a { color: ${linkColor} }`); } const linkActiveColor = theme.getColor(textLinkActiveForeground); if (linkActiveColor) { - content.push(`.monaco-editor .review-widget .body .review-comment a:hover, a:active { color: ${linkActiveColor} }`); + content.push(`.monaco-editor .review-widget .body .comment-body a:hover, a:active { color: ${linkActiveColor} }`); } const focusColor = theme.getColor(focusBorder); if (focusColor) { - content.push(`.monaco-editor .review-widget .body .review-comment a:focus { outline: 1px solid ${focusColor}; }`); - content.push(`.monaco-editor .review-widget .body .comment-form .monaco-editor.focused { outline: 1px solid ${focusColor}; }`); + content.push(`.monaco-editor .review-widget .body .comment-body a:focus { outline: 1px solid ${focusColor}; }`); + content.push(`.monaco-editor .review-widget .body .monaco-editor.focused { outline: 1px solid ${focusColor}; }`); } const blockQuoteBackground = theme.getColor(textBlockQuoteBackground); @@ -619,17 +586,17 @@ export class ReviewZoneWidget extends ZoneWidget { const hcBorder = theme.getColor(contrastBorder); if (hcBorder) { content.push(`.monaco-editor .review-widget .body .comment-form .review-thread-reply-button { outline-color: ${hcBorder}; }`); - content.push(`.monaco-editor .review-widget .body .comment-form .monaco-editor { outline: 1px solid ${hcBorder}; }`); + content.push(`.monaco-editor .review-widget .body .monaco-editor { outline: 1px solid ${hcBorder}; }`); } const errorBorder = theme.getColor(inputValidationErrorBorder); if (errorBorder) { - content.push(`.monaco-editor .review-widget .body .comment-form .validation-error { border: 1px solid ${errorBorder}; }`); + content.push(`.monaco-editor .review-widget .validation-error { border: 1px solid ${errorBorder}; }`); } const errorBackground = theme.getColor(inputValidationErrorBackground); if (errorBackground) { - content.push(`.monaco-editor .review-widget .body .comment-form .validation-error { background: ${errorBackground}; }`); + content.push(`.monaco-editor .review-widget .validation-error { background: ${errorBackground}; }`); } const errorForeground = theme.getColor(inputValidationErrorForeground); diff --git a/src/vs/workbench/parts/comments/electron-browser/media/review.css b/src/vs/workbench/parts/comments/electron-browser/media/review.css index c3bd18b08e036136014cda41c493ae7d312f170a..ee23244f647844ec787c75658168c55fa0ab1271 100644 --- a/src/vs/workbench/parts/comments/electron-browser/media/review.css +++ b/src/vs/workbench/parts/comments/electron-browser/media/review.css @@ -30,12 +30,33 @@ position: absolute; } +.monaco-editor .review-widget .hidden { + display: none !important; +} + .monaco-editor .review-widget .body .review-comment { padding: 8px 16px 8px 20px; display: flex; } -.monaco-editor .review-widget .body .review-comment blockquote { +.monaco-editor .review-widget .body .review-comment .comment-actions { + margin-left: auto; +} + +.monaco-editor .review-widget .body .review-comment .comment-actions .action-item { + width: 30px; +} + +.monaco-editor .review-widget .body .review-comment .comment-title { + display: flex; + width: 100%; +} + +.monaco-editor .review-widget .body .review-comment .comment-title .action-label.octicon { + line-height: 18px; +} + +.monaco-editor .review-widget .body .comment-body blockquote { margin: 0 7px 0 5px; padding: 0 16px 0 10px; border-left-width: 5px; @@ -58,8 +79,10 @@ } .monaco-editor .review-widget .body .review-comment .review-comment-contents { - margin-left: 20px; + padding-left: 20px; user-select: text; + width: 100%; + overflow: hidden; } .monaco-editor .review-widget .body pre { @@ -68,13 +91,14 @@ white-space: pre; } -.monaco-editor.vs-dark .review-widget .body .review-comment .review-comment-contents h4 { +.monaco-editor.vs-dark .review-widget .body .comment-body h4 { margin: 0; } .monaco-editor.vs-dark .review-widget .body .review-comment .review-comment-contents .author { color: #fff; font-weight: 600; + line-height: 19px; } .monaco-editor.vs-dark .review-widget .body .review-comment .review-comment-contents .comment-body { @@ -85,39 +109,39 @@ color: #e0e0e0; } -.monaco-editor .review-widget .body p, -.monaco-editor .review-widget .body ul { +.monaco-editor .review-widget .body .comment-body p, +.monaco-editor .review-widget .body .comment-body ul { margin: 8px 0; } -.monaco-editor .review-widget .body p:first-child, -.monaco-editor .review-widget .body ul:first-child { +.monaco-editor .review-widget .body .comment-body p:first-child, +.monaco-editor .review-widget .body .comment-body ul:first-child { margin-top: 0; } -.monaco-editor .review-widget .body p:last-child, -.monaco-editor .review-widget .body ul:last-child { +.monaco-editor .review-widget .body .comment-body p:last-child, +.monaco-editor .review-widget .body.comment-body ul:last-child { margin-bottom: 0; } -.monaco-editor .review-widget .body ul { +.monaco-editor .review-widget .body .comment-body ul { padding-left: 20px; } -.monaco-editor .review-widget .body li>p { +.monaco-editor .review-widget .body .comment-body li>p { margin-bottom: 0; } -.monaco-editor .review-widget .body li>ul { +.monaco-editor .review-widget .body .comment-body li>ul { margin-top: 0; } -.monaco-editor .review-widget .body code { +.monaco-editor .review-widget .body .comment-body code { border-radius: 3px; padding: 0 0.4em; } -.monaco-editor .review-widget .body span { +.monaco-editor .review-widget .body .comment-body span { white-space: pre; } @@ -130,7 +154,7 @@ padding: 8px 0; } -.monaco-editor .review-widget .body .comment-form .validation-error { +.monaco-editor .review-widget .validation-error { display: inline-block; overflow: hidden; text-align: left; @@ -149,10 +173,6 @@ word-wrap: break-word; } -.monaco-editor .review-widget .body .comment-form .validation-error.hidden { - display: none; -} - .monaco-editor .review-widget .body .comment-form.expand .review-thread-reply-button { display: none; } @@ -184,25 +204,46 @@ outline-width: 1px; } -.monaco-editor .review-widget .body .comment-form .monaco-editor { - display: none; +.monaco-editor .review-widget .body .comment-form .monaco-editor, +.monaco-editor .review-widget .body .edit-container .monaco-editor { width: 100%; min-height: 90px; max-height: 500px; border-radius: 3px; border: 0px; + box-sizing: content-box; padding: 6px 0 6px 12px; } +.monaco-editor .review-widget .body .comment-form .monaco-editor, .monaco-editor .review-widget .body .comment-form .form-actions { + display: none; +} + +.monaco-editor .review-widget .body .comment-form .form-actions, +.monaco-editor .review-widget .body .edit-container .form-actions { overflow: auto; padding: 10px 0; - display: none; } -.monaco-editor .review-widget .body .comment-form .monaco-text-button { +.monaco-editor .review-widget .body .edit-container .form-actions { + display: flex; + justify-content: flex-end; +} + +.monaco-editor .review-widget .body .edit-textarea { + height: 90px; + margin: 5px 0 10px 0; +} + +.monaco-editor .review-widget .body .comment-form .monaco-text-button, +.monaco-editor .review-widget .body .edit-container .monaco-text-button { width: auto; padding: 4px 10px; + margin-left: 5px; +} + +.monaco-editor .review-widget .body .comment-form .monaco-text-button { float: right; }