diff --git a/extensions/git-extended/src/common/models/comment.ts b/extensions/git-extended/src/common/models/comment.ts index 9665c6caf720774f840e8f8288a04621cf899469..3e973e1a3ad185c397d251ab5c02ba1a6594528f 100644 --- a/extensions/git-extended/src/common/models/comment.ts +++ b/extensions/git-extended/src/common/models/comment.ts @@ -14,7 +14,9 @@ export interface DiffHunkRange { export interface User { id: string; login: string; + avatar_url: string; } + export interface Comment { url: string; id: string; diff --git a/extensions/git-extended/src/prProvider.ts b/extensions/git-extended/src/prProvider.ts index 5f4a94ec0706d55ff2254cb262feb8e81efdeece..56942e1a236e3b9cc1db4931e63f28bb8e233cad 100644 --- a/extensions/git-extended/src/prProvider.ts +++ b/extensions/git-extended/src/prProvider.ts @@ -135,11 +135,24 @@ export class PRProvider implements vscode.TreeDataProvider { const octo = await this.crendentialStore.getOctokit(remote); if (octo) { - const { data } = await octo.pullRequests.getAll({ + let { data } = await octo.pullRequests.getAll({ owner: remote.owner, - repo: remote.name + repo: remote.name, + per_page: 100 }); - return data.map(item => new PullRequest(octo, remote, item)); + let ret = data.map(item => new PullRequest(octo, remote, item)); + + if (ret.length >= 100) { + let secondPage = await octo.pullRequests.getAll({ + owner: remote.owner, + repo: remote.name, + per_page: 100, + page: 2 + }); + + ret.push(...secondPage.data.map(item => new PullRequest(octo, remote, item))); + } + return ret; } }); @@ -204,7 +217,8 @@ export class PRProvider implements vscode.TreeDataProvider { return { body: new vscode.MarkdownString(comment.body), - userName: comment.user.login + userName: comment.user.login, + gravatar: comment.user.avatar_url }; }) }); diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index c77b0a7137bac8cab8a6c5bf4ef3fa79b1ae12c2..d5057babb40d0f13989a0714a9cb6129f00fb204 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -946,6 +946,7 @@ export interface CommentThread { export interface Comment { readonly body: IMarkdownString; readonly userName: string; + readonly gravatar: string; } export interface CommentProvider { diff --git a/src/vs/editor/contrib/review/close.svg b/src/vs/editor/contrib/review/close.svg new file mode 100644 index 0000000000000000000000000000000000000000..751e89b3b0215f74d84195d1dea54ca0c25f91c3 --- /dev/null +++ b/src/vs/editor/contrib/review/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/review/review.css b/src/vs/editor/contrib/review/review.css index 3b40188c50a04b4d7e2db58b806625faa5bca627..443169f61ee735cd2562e724e0bca55bdd25c163 100644 --- a/src/vs/editor/contrib/review/review.css +++ b/src/vs/editor/contrib/review/review.css @@ -13,50 +13,143 @@ .monaco-editor .review-widget { width: 100%; position: absolute; - margin: 0px 20px 20px 20px; } -.monaco-editor .review-widget pre { + +.monaco-editor .review-widget .body .review-comment { + margin-left: 20px; + padding: 8px 16px 8px 0px; +} + +.monaco-editor .review-widget .body .review-comment .float-left { + float: left; + margin-top: 4px !important; +} +.monaco-editor .review-widget .body .review-comment .float-left img.avatar { + height: 28px; + width: 28px; + display: inline-block; + overflow: hidden; + line-height: 1; + vertical-align: middle; + border-radius: 3px; + border-style: none; +} + +.monaco-editor .review-widget .body .review-comment .review-comment-contents { + margin-left: 44px; +} + +.monaco-editor .review-widget .body pre { overflow: auto; word-wrap: normal; white-space: pre; } -.monaco-editor.vs-dark .review-widget .author { +.monaco-editor.vs-dark .review-widget .body .review-comment .review-comment-contents h4 { + margin: 0; +} +.monaco-editor.vs-dark .review-widget .body .review-comment .review-comment-contents .author { color: #fff; + font-weight: 600; +} +.monaco-editor.vs-dark .review-widget .body .review-comment .review-comment-contents .comment-body { + padding-top: 4px; } -.monaco-editor.vs-dark .review-widget span.created_at { +.monaco-editor.vs-dark .review-widget .body span.created_at { color: #e0e0e0; } -.monaco-editor .review-widget p, -.monaco-editor .review-widget ul { +.monaco-editor .review-widget .body p, +.monaco-editor .review-widget .body ul { margin: 8px 0; } -.monaco-editor .review-widget p:first-child, -.monaco-editor .review-widget ul:first-child { +.monaco-editor .review-widget .body p:first-child, +.monaco-editor .review-widget .body ul:first-child { margin-top: 0; } -.monaco-editor .review-widget p:last-child, -.monaco-editor .review-widget ul:last-child { +.monaco-editor .review-widget .body p:last-child, +.monaco-editor .review-widget .body ul:last-child { margin-bottom: 0; } -.monaco-editor .review-widget ul { +.monaco-editor .review-widget .body ul { padding-left: 20px; } -.monaco-editor .review-widget li > p { +.monaco-editor .review-widget .body li > p { margin-bottom: 0; } -.monaco-editor .review-widget li > ul { +.monaco-editor .review-widget .body li > ul { margin-top: 0; } -.monaco-editor .review-widget code { +.monaco-editor .review-widget .body code { border-radius: 3px; padding: 0 0.4em; -} \ No newline at end of file +} + +.monaco-editor .review-widget .head { + -webkit-box-sizing: border-box; + -o-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + display: flex; +} + +.monaco-editor .review-widget .head .review-title { + display: inline-block; + font-size: 13px; + margin-left: 20px; + cursor: pointer; +} + +.monaco-editor .review-widget .head .review-title .dirname:not(:empty) { + font-size: 0.9em; + margin-left: 0.5em; +} + +.monaco-editor .review-widget .head .review-actions { + flex: 1; + text-align: right; + padding-right: 2px; +} + +.monaco-editor .review-widget .head .review-actions > .monaco-action-bar { + display: inline-block; +} + +.monaco-editor .review-widget .head .review-actions > .monaco-action-bar, +.monaco-editor .review-widget .head .review-actions > .monaco-action-bar > .actions-container { + height: 100%; +} + +.monaco-editor .review-widget .head .review-actions > .monaco-action-bar .action-item { + margin-left: 4px; +} + +.monaco-editor .review-widget .head .review-actions > .monaco-action-bar .action-label { + width: 16px; + height: 100%; + margin: 0; + line-height: inherit; + background-repeat: no-repeat; + background-position: center center; +} + +.monaco-editor .review-widget .head .review-actions > .monaco-action-bar .action-label.octicon { + margin: 0; +} + +.monaco-editor .review-widget .head .review-actions .action-label.icon.close-review-action { + background: url('close.svg') center center no-repeat; +} + +.monaco-editor .review-widget > .body { + border-top: 1px solid; + position: relative; +} diff --git a/src/vs/editor/contrib/review/review.ts b/src/vs/editor/contrib/review/review.ts index 4166acf7306997d2dc1365a5bec79a55a1f1ee01..cce88dfd45293fd3a592cf47fac8e06b0861ef1e 100644 --- a/src/vs/editor/contrib/review/review.ts +++ b/src/vs/editor/contrib/review/review.ts @@ -5,9 +5,11 @@ 'use strict'; import 'vs/css!./review'; +import * as nls from 'vs/nls'; import * as modes from 'vs/editor/common/modes'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType, IViewZone } from 'vs/editor/browser/editorBrowser'; +import { $ } from 'vs/base/browser/builder'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; @@ -20,6 +22,11 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Action } from 'vs/base/common/actions'; +import { registerThemingParticipant, ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; +import { peekViewEditorBackground, peekViewBorder, } from 'vs/editor/contrib/referenceSearch/referencesWidget'; +import { Color } from 'vs/base/common/color'; export const ctxReviewPanelVisible = new RawContextKey('reviewPanelVisible', false); export const ID = 'editor.contrib.review'; @@ -28,7 +35,7 @@ declare var ResizeObserver: any; const REVIEWL_DECORATION = ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - linesDecorationsClassName: 'review' + glyphMarginClassName: 'review' }); export class ReviewViewZone implements IViewZone { @@ -50,27 +57,85 @@ export class ReviewViewZone implements IViewZone { } export class ReviewZoneWidget extends ZoneWidget { - private _domNode: HTMLElement; + private _headElement: HTMLElement; + protected _primaryHeading: HTMLElement; + protected _secondaryHeading: HTMLElement; + protected _metaHeading: HTMLElement; + protected _actionbarWidget: ActionBar; + private _bodyElement: HTMLElement; private _resizeObserver: any; + private _comments: modes.Comment[]; - constructor(editor: ICodeEditor, options: IOptions = {}) { + constructor(@IThemeService private themeService: IThemeService, + editor: ICodeEditor, options: IOptions = {}, comments: modes.Comment[]) { super(editor, options); this._resizeObserver = null; + this._comments = comments; this.create(); + this.themeService.onThemeChange(this._applyTheme, this); } protected _fillContainer(container: HTMLElement): void { - this._domNode = document.createElement('div'); - this._domNode.className = 'review-widget'; - container.appendChild(this._domNode); + this.setCssClass('review-widget'); + this._headElement = $('.head').getHTMLElement(); + container.appendChild(this._headElement); + this._fillHead(this._headElement); + + this._bodyElement = $('.body').getHTMLElement(); document.createElement('div'); + container.appendChild(this._bodyElement); + } + + protected _fillHead(container: HTMLElement): void { + var titleElement = $('.review-title'). + // on(dom.EventType.CLICK, e => this._onTitleClick(e)). + appendTo(this._headElement). + getHTMLElement(); + + this._primaryHeading = $('span.filename').appendTo(titleElement).getHTMLElement(); + this._secondaryHeading = $('span.dirname').appendTo(titleElement).getHTMLElement(); + this._metaHeading = $('span.meta').appendTo(titleElement).getHTMLElement(); + + let primaryHeading = 'Discussion'; + $(this._primaryHeading).safeInnerHtml(primaryHeading); + this._primaryHeading.setAttribute('aria-label', primaryHeading); + let secondaryHeading = `@${this._comments[0].userName}`; + $(this._secondaryHeading).safeInnerHtml(secondaryHeading); + + const actionsContainer = $('.review-actions').appendTo(this._headElement); + this._actionbarWidget = new ActionBar(actionsContainer, {}); + this._disposables.push(this._actionbarWidget); + + this._actionbarWidget.push(new Action('review.expand', nls.localize('label.expand', "Expand"), 'expand-review-action octicon octicon-chevron-down', true, () => { + this._bodyElement.style.display = 'block'; + return null; + }), { label: false, icon: true }); + + this._actionbarWidget.push(new Action('review.close', nls.localize('label.close', "Close"), 'close-review-action', true, () => { + this.dispose(); + return null; + }), { label: false, icon: true }); + } display(comments: modes.Comment[], lineNumber: number) { this.show({ lineNumber: lineNumber, column: 1 }, 2); + this._bodyElement.style.display = 'none'; for (let i = 0; i < comments.length; i++) { let singleCommentContainer = document.createElement('div'); - singleCommentContainer.className = 'review-comment-contents'; + singleCommentContainer.className = 'review-comment'; + let avatar = document.createElement('span'); + avatar.className = 'float-left'; + let img = document.createElement('img'); + img.className = 'avatar'; + img.src = comments[i].gravatar; + avatar.appendChild(img); + let commentDetailsContainer = document.createElement('div'); + commentDetailsContainer.className = 'review-comment-contents'; + + singleCommentContainer.appendChild(avatar); + singleCommentContainer.appendChild(commentDetailsContainer); + let header = document.createElement('h4'); let author = document.createElement('strong'); author.className = 'author'; @@ -80,17 +145,17 @@ export class ReviewZoneWidget extends ZoneWidget { // time.innerText = comments[i].created_at; header.appendChild(author); // header.appendChild(time); - singleCommentContainer.appendChild(header); + commentDetailsContainer.appendChild(header); let body = document.createElement('div'); body.className = 'comment-body'; - singleCommentContainer.appendChild(body); + commentDetailsContainer.appendChild(body); let md = comments[i].body; body.appendChild(renderMarkdown(md)); - this._domNode.appendChild(singleCommentContainer); + this._bodyElement.appendChild(singleCommentContainer); } // this._domNode.appendChild(document.createElement('textarea')); this._resizeObserver = new ResizeObserver(entries => { - if (entries[0].target === this._domNode) { + if (entries[0].target === this._bodyElement) { const lineHeight = this.editor.getConfiguration().lineHeight; const arrowHeight = Math.round(lineHeight / 3); const computedLinesNumber = Math.ceil((entries[0].contentRect.height + arrowHeight + 30) / lineHeight); @@ -98,14 +163,25 @@ export class ReviewZoneWidget extends ZoneWidget { } }); - this._resizeObserver.observe(this._domNode); + this._resizeObserver.observe(this._bodyElement); + } + + private _applyTheme(theme: ITheme) { + let borderColor = theme.getColor(peekViewBorder) || Color.transparent; + this.style({ + arrowColor: borderColor, + frameColor: borderColor + }); } dispose() { super.dispose(); - this._resizeObserver.disconnect(); - this._resizeObserver = null; + if (this._resizeObserver) { + this._resizeObserver.disconnect(); + this._resizeObserver = null; + } } + } export class ReviewController implements IEditorContribution { @@ -115,12 +191,14 @@ export class ReviewController implements IEditorContribution { private decorationIDs: string[]; private _domNode: HTMLElement; private _zoneWidget: ReviewZoneWidget; + private _zoneWidgets: ReviewZoneWidget[]; private _reviewPanelVisible: IContextKey; private _commentThreads: modes.CommentThread[]; constructor( editor: ICodeEditor, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IThemeService private themeService: IThemeService ) { this.editor = editor; this.globalToDispose = []; @@ -128,6 +206,7 @@ export class ReviewController implements IEditorContribution { this.decorationIDs = []; this.mouseDownInfo = null; this._commentThreads = []; + this._zoneWidgets = []; this._reviewPanelVisible = ctxReviewPanelVisible.bindTo(contextKeyService); this._domNode = document.createElement('div'); @@ -162,6 +241,11 @@ export class ReviewController implements IEditorContribution { this._zoneWidget.dispose(); this._zoneWidget = null; } + + this._zoneWidgets.forEach(zone => { + zone.dispose(); + }); + this._zoneWidgets = []; this.localToDispose.push(this.editor.onMouseDown(e => this.onEditorMouseDown(e))); this.localToDispose.push(this.editor.onMouseUp(e => this.onEditorMouseUp(e))); } @@ -180,7 +264,7 @@ export class ReviewController implements IEditorContribution { let iconClicked = false; switch (e.target.type) { - case MouseTargetType.GUTTER_LINE_DECORATIONS: + case MouseTargetType.GUTTER_GLYPH_MARGIN: iconClicked = true; break; default: @@ -203,7 +287,7 @@ export class ReviewController implements IEditorContribution { } if (iconClicked) { - if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) { + if (e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN) { return; } } @@ -215,7 +299,7 @@ export class ReviewController implements IEditorContribution { let comments = this.getComments(lineNumber); if (comments && comments.length) { this._reviewPanelVisible.set(true); - this._zoneWidget = new ReviewZoneWidget(this.editor); + this._zoneWidget = new ReviewZoneWidget(this.themeService, this.editor, {}, comments); this._zoneWidget.display(this.getComments(lineNumber), lineNumber); } } @@ -238,6 +322,17 @@ export class ReviewController implements IEditorContribution { options: REVIEWL_DECORATION }))); }); + + // create viewzones + this._zoneWidgets.forEach(zone => { + zone.dispose(); + }); + + this._commentThreads.forEach(thread => { + let zoneWidget = new ReviewZoneWidget(this.themeService, this.editor, {}, thread.comments); + zoneWidget.display(this.getComments(thread.range.startLineNumber), thread.range.startLineNumber); + }); + } @@ -286,4 +381,16 @@ function closeReviewPanel(accessor: ServicesAccessor, args: any) { } controller.closeWidget(); -} \ No newline at end of file +} + + +registerThemingParticipant((theme, collector) => { + let editorBackground = theme.getColor(peekViewEditorBackground); + if (editorBackground) { + collector.addRule( + `.monaco-editor .review-widget,` + + `.monaco-editor .review-widget {` + + ` background-color: ${editorBackground};` + + `}`); + } +}); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 2e8a7e01f79dd9f0b164c9ad13d38b7a2f4ca352..d40c18026a1f061fde0cfad7becb834c2f288803 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5006,6 +5006,7 @@ declare namespace monaco.languages { export interface Comment { readonly body: IMarkdownString; readonly userName: string; + readonly gravatar: string; } export interface CommentProvider { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index d2429fec4280040f72cde45643768f0e044bafa5..5a22e718ca7cfc054db7f55791b1a5fe1b501d28 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -732,6 +732,7 @@ declare module 'vscode' { interface Comment { body: MarkdownString; userName: string; + gravatar: string; } /** diff --git a/src/vs/workbench/api/node/extHostComments.ts b/src/vs/workbench/api/node/extHostComments.ts index df16c6bbf99426342a5ee07a18c120c0a4e887b9..1c49f1505b9204dbdebf6d7050daed1e20ec41b5 100644 --- a/src/vs/workbench/api/node/extHostComments.ts +++ b/src/vs/workbench/api/node/extHostComments.ts @@ -72,6 +72,7 @@ function convertCommentThread(vscodeCommentThread: vscode.CommentThread): modes. function convertComment(vscodeComment: vscode.Comment): modes.Comment { return { body: extHostTypeConverter.MarkdownString.from(vscodeComment.body), - userName: vscodeComment.userName + userName: vscodeComment.userName, + gravatar: vscodeComment.gravatar }; }