commentThreadWidget.ts 33.0 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
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 * as arrays from 'vs/base/common/arrays';
import { Color } from 'vs/base/common/color';
import { Emitter, Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
15
import * as platform from 'vs/base/common/platform';
16
import * as strings from 'vs/base/common/strings';
17 18 19 20 21 22
import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser';
import * as modes from 'vs/editor/common/modes';
import { peekViewBorder } from 'vs/editor/contrib/referenceSearch/referencesWidget';
import { IOptions, ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget';
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService';
23
import { CommentGlyphWidget } from 'vs/workbench/contrib/comments/electron-browser/commentGlyphWidget';
P
Peng Lyu 已提交
24 25 26
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IModelService } from 'vs/editor/common/services/modelService';
import { SimpleCommentEditor } from './simpleCommentEditor';
27
import { URI } from 'vs/base/common/uri';
28
import { transparent, editorForeground, textLinkActiveForeground, textLinkForeground, focusBorder, textBlockQuoteBackground, textBlockQuoteBorder, contrastBorder, inputValidationErrorBorder, inputValidationErrorBackground, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry';
P
Peng Lyu 已提交
29 30
import { IModeService } from 'vs/editor/common/services/modeService';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
31
import { KeyCode } from 'vs/base/common/keyCodes';
32
import { ICommentService } from 'vs/workbench/contrib/comments/electron-browser/commentService';
33 34
import { Range, IRange } from 'vs/editor/common/core/range';
import { IPosition } from 'vs/editor/common/core/position';
35 36
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer';
37
import { IMarginData } from 'vs/editor/browser/controller/mouseTarget';
38
import { CommentNode } from 'vs/workbench/contrib/comments/electron-browser/commentNode';
39
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
40
import { INotificationService } from 'vs/platform/notification/common/notification';
R
rebornix 已提交
41
import { ITextModel } from 'vs/editor/common/model';
P
Peng Lyu 已提交
42
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
P
Peng Lyu 已提交
43
import { ICommandService } from 'vs/platform/commands/common/commands';
44
import { generateUuid } from 'vs/base/common/uuid';
45

P
Peng Lyu 已提交
46
export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration';
47
const COLLAPSE_ACTION_CLASS = 'expand-review-action octicon octicon-x';
P
Peng Lyu 已提交
48
const COMMENT_SCHEME = 'comment';
49

50
let INMEM_MODEL_ID = 0;
51

52 53
export class ReviewZoneWidget extends ZoneWidget {
	private _headElement: HTMLElement;
54
	protected _headingLabel: HTMLElement;
55 56
	protected _actionbarWidget: ActionBar;
	private _bodyElement: HTMLElement;
P
Peng Lyu 已提交
57
	private _commentEditor: ICodeEditor;
58 59
	private _commentsElement: HTMLElement;
	private _commentElements: CommentNode[];
60 61
	private _commentForm: HTMLElement;
	private _reviewThreadReplyButton: HTMLElement;
62 63
	private _resizeObserver: any;
	private _onDidClose = new Emitter<ReviewZoneWidget>();
64
	private _onDidCreateThread = new Emitter<ReviewZoneWidget>();
65
	private _isCollapsed;
66
	private _collapseAction: Action;
P
Peng Lyu 已提交
67
	private _commentThread: modes.CommentThread | modes.CommentThread2;
68
	private _commentGlyph: CommentGlyphWidget;
69
	private _owner: string;
70
	private _pendingComment: string;
R
rebornix 已提交
71
	private _draftMode: modes.DraftMode;
72
	private _localToDispose: IDisposable[];
73
	private _globalToDispose: IDisposable[];
74 75
	private _markdownRenderer: MarkdownRenderer;
	private _styleElement: HTMLStyleElement;
76
	private _formActions: HTMLElement;
77
	private _error: HTMLElement;
78

79
	public get owner(): string {
80 81
		return this._owner;
	}
82 83 84
	public get commentThread(): modes.CommentThread {
		return this._commentThread;
	}
85

86 87 88 89
	public get extensionId(): string {
		return this._commentThread.extensionId;
	}

90 91 92 93
	public get draftMode(): modes.DraftMode {
		return this._draftMode;
	}

94
	constructor(
95 96
		private instantiationService: IInstantiationService,
		private modeService: IModeService,
P
Peng Lyu 已提交
97
		private commandService: ICommandService,
98 99 100
		private modelService: IModelService,
		private themeService: IThemeService,
		private commentService: ICommentService,
101
		private openerService: IOpenerService,
102
		private dialogService: IDialogService,
103
		private notificationService: INotificationService,
P
Peng Lyu 已提交
104
		private contextMenuService: IContextMenuService,
105
		editor: ICodeEditor,
106
		owner: string,
P
Peng Lyu 已提交
107
		commentThread: modes.CommentThread | modes.CommentThread2,
108
		pendingComment: string,
R
rebornix 已提交
109
		draftMode: modes.DraftMode,
110
		options: IOptions = { keepEditorSelection: true }
111 112 113 114 115
	) {
		super(editor, options);
		this._resizeObserver = null;
		this._owner = owner;
		this._commentThread = commentThread;
116
		this._pendingComment = pendingComment;
R
rebornix 已提交
117
		this._draftMode = draftMode;
118
		this._isCollapsed = commentThread.collapsibleState !== modes.CommentThreadCollapsibleState.Expanded;
119
		this._globalToDispose = [];
120
		this._localToDispose = [];
121
		this._formActions = null;
122
		this.create();
123 124

		this._styleElement = dom.createStyleSheet(this.domNode);
125 126 127 128 129 130
		this._globalToDispose.push(this.themeService.onThemeChange(this._applyTheme, this));
		this._globalToDispose.push(this.editor.onDidChangeConfiguration(e => {
			if (e.fontInfo) {
				this._applyTheme(this.themeService.getTheme());
			}
		}));
131 132 133
		this._applyTheme(this.themeService.getTheme());

		this._markdownRenderer = new MarkdownRenderer(editor, this.modeService, this.openerService);
134 135 136 137 138 139
	}

	public get onDidClose(): Event<ReviewZoneWidget> {
		return this._onDidClose.event;
	}

140 141 142 143
	public get onDidCreateThread(): Event<ReviewZoneWidget> {
		return this._onDidCreateThread.event;
	}

144 145 146 147 148 149 150 151 152 153
	public getPosition(): IPosition | undefined {
		let position: IPosition = this.position;
		if (position) {
			return position;
		}

		position = this._commentGlyph.getPosition().position;
		return position;
	}

P
Peng Lyu 已提交
154 155 156 157 158
	protected revealLine(lineNumber: number) {
		// we don't do anything here as we always do the reveal ourselves.
	}

	public reveal(commentId?: string) {
159
		if (this._isCollapsed) {
160
			this.show({ lineNumber: this._commentThread.range.startLineNumber, column: 1 }, 2);
161
		}
162

P
Peng Lyu 已提交
163 164 165 166 167 168 169 170 171 172 173 174
		if (commentId) {
			let height = this.editor.getLayoutInfo().height;
			let matchedNode = this._commentElements.filter(commentNode => commentNode.comment.commentId === commentId);
			if (matchedNode && matchedNode.length) {
				const commentThreadCoords = dom.getDomNodePagePosition(this._commentElements[0].domNode);
				const commentCoords = dom.getDomNodePagePosition(matchedNode[0].domNode);

				this.editor.setScrollTop(this.editor.getTopForLineNumber(this._commentThread.range.startLineNumber) - height / 2 + commentCoords.top - commentThreadCoords.top);
				return;
			}
		}

175
		this.editor.revealRangeInCenter(this._commentThread.range);
176 177
	}

178 179 180 181
	public getPendingComment(): string {
		if (this._commentEditor) {
			let model = this._commentEditor.getModel();

182
			if (model && model.getValueLength() > 0) { // checking length is cheap
183 184 185 186 187 188 189
				return model.getValue();
			}
		}

		return null;
	}

190 191
	protected _fillContainer(container: HTMLElement): void {
		this.setCssClass('review-widget');
192
		this._headElement = <HTMLDivElement>dom.$('.head');
193 194 195
		container.appendChild(this._headElement);
		this._fillHead(this._headElement);

196
		this._bodyElement = <HTMLDivElement>dom.$('.body');
197
		container.appendChild(this._bodyElement);
198 199 200 201

		dom.addDisposableListener(this._bodyElement, dom.EventType.FOCUS_IN, e => {
			this.commentService.setActiveCommentThread(this._commentThread);
		});
202 203 204
	}

	protected _fillHead(container: HTMLElement): void {
M
Matt Bierner 已提交
205
		let titleElement = dom.append(this._headElement, dom.$('.review-title'));
206

207
		this._headingLabel = dom.append(titleElement, dom.$('span.filename'));
208
		this.createThreadLabel();
209

210 211
		const actionsContainer = dom.append(this._headElement, dom.$('.review-actions'));
		this._actionbarWidget = new ActionBar(actionsContainer, {});
212 213
		this._disposables.push(this._actionbarWidget);

214
		this._collapseAction = new Action('review.expand', nls.localize('label.collapse', "Collapse"), COLLAPSE_ACTION_CLASS, true, () => {
P
Peng Lyu 已提交
215
			if (this._commentThread.comments.length === 0 && (this._commentThread as modes.CommentThread2).commentThreadHandle === undefined) {
216 217
				this.dispose();
				return null;
218
			}
219

220 221
			this._isCollapsed = true;
			this.hide();
222 223 224
			return null;
		});

225
		this._actionbarWidget.push(this._collapseAction, { label: false, icon: true });
226 227 228
	}

	toggleExpand() {
229
		this._collapseAction.run();
230 231
	}

P
Peng Lyu 已提交
232
	update(commentThread: modes.CommentThread | modes.CommentThread2) {
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
		const oldCommentsLen = this._commentElements.length;
		const newCommentsLen = commentThread.comments.length;

		let commentElementsToDel: CommentNode[] = [];
		let commentElementsToDelIndex: number[] = [];
		for (let i = 0; i < oldCommentsLen; i++) {
			let comment = this._commentElements[i].comment;
			let newComment = commentThread.comments.filter(c => c.commentId === comment.commentId);

			if (newComment.length) {
				this._commentElements[i].update(newComment[0]);
			} else {
				commentElementsToDelIndex.push(i);
				commentElementsToDel.push(this._commentElements[i]);
			}
		}

		// del removed elements
		for (let i = commentElementsToDel.length - 1; i >= 0; i--) {
I
Ilya Biryukov 已提交
252
			this._commentElements.splice(commentElementsToDelIndex[i], 1);
253 254 255
			this._commentsElement.removeChild(commentElementsToDel[i].domNode);
		}

256
		let lastCommentElement: HTMLElement | null = null;
257 258 259 260 261
		let newCommentNodeList: CommentNode[] = [];
		for (let i = newCommentsLen - 1; i >= 0; i--) {
			let currentComment = commentThread.comments[i];
			let oldCommentNode = this._commentElements.filter(commentNode => commentNode.comment.commentId === currentComment.commentId);
			if (oldCommentNode.length) {
R
rebornix 已提交
262
				oldCommentNode[0].update(currentComment);
263 264 265
				lastCommentElement = oldCommentNode[0].domNode;
				newCommentNodeList.unshift(oldCommentNode[0]);
			} else {
266
				const newElement = this.createNewCommentNode(currentComment);
267

268 269 270 271 272 273 274 275 276 277 278 279 280
				newCommentNodeList.unshift(newElement);
				if (lastCommentElement) {
					this._commentsElement.insertBefore(newElement.domNode, lastCommentElement);
					lastCommentElement = newElement.domNode;
				} else {
					this._commentsElement.appendChild(newElement.domNode);
					lastCommentElement = newElement.domNode;
				}
			}
		}

		this._commentThread = commentThread;
		this._commentElements = newCommentNodeList;
281
		this.createThreadLabel();
I
Ilya Biryukov 已提交
282 283 284 285 286

		// Move comment glyph widget and show position if the line has changed.
		const lineNumber = this._commentThread.range.startLineNumber;
		if (this._commentGlyph.getPosition().position.lineNumber !== lineNumber) {
			this._commentGlyph.setLineNumber(lineNumber);
287 288
		}

P
Peng Lyu 已提交
289 290 291 292
		if (!this._reviewThreadReplyButton) {
			this.createReplyButton();
		}

293 294
		if (!this._isCollapsed) {
			this.show({ lineNumber, column: 1 }, 2);
I
Ilya Biryukov 已提交
295
		}
296 297
	}

298 299 300 301 302 303 304 305 306 307
	updateDraftMode(draftMode: modes.DraftMode) {
		this._draftMode = draftMode;

		if (this._formActions) {
			let model = this._commentEditor.getModel();
			dom.clearNode(this._formActions);
			this.createCommentWidgetActions(this._formActions, model);
		}
	}

308 309 310 311
	protected _onWidth(widthInPixel: number): void {
		this._commentEditor.layout({ height: (this._commentEditor.hasWidgetFocus() ? 5 : 1) * 18, width: widthInPixel - 54 /* margin 20px * 10 + scrollbar 14px*/ });
	}

P
Peng Lyu 已提交
312
	protected _doLayout(heightInPixel: number, widthInPixel: number): void {
313
		this._commentEditor.layout({ height: (this._commentEditor.hasWidgetFocus() ? 5 : 1) * 18, width: widthInPixel - 54 /* margin 20px * 10 + scrollbar 14px*/ });
P
Peng Lyu 已提交
314 315
	}

316 317
	display(lineNumber: number) {
		this._commentGlyph = new CommentGlyphWidget(this.editor, lineNumber);
318 319 320

		this._localToDispose.push(this.editor.onMouseDown(e => this.onEditorMouseDown(e)));
		this._localToDispose.push(this.editor.onMouseUp(e => this.onEditorMouseUp(e)));
M
Matt Bierner 已提交
321
		let headHeight = Math.ceil(this.editor.getConfiguration().lineHeight * 1.2);
322 323 324
		this._headElement.style.height = `${headHeight}px`;
		this._headElement.style.lineHeight = this._headElement.style.height;

325
		this._commentsElement = dom.append(this._bodyElement, dom.$('div.comments-container'));
P
Peng Lyu 已提交
326 327
		this._commentsElement.setAttribute('role', 'presentation');

328
		this._commentElements = [];
329 330
		for (const comment of this._commentThread.comments) {
			const newCommentNode = this.createNewCommentNode(comment);
331

332 333 334 335
			this._commentElements.push(newCommentNode);
			this._commentsElement.appendChild(newCommentNode.domNode);
		}

336
		const hasExistingComments = this._commentThread.comments.length > 0;
337
		this._commentForm = dom.append(this._bodyElement, dom.$('.comment-form'));
338
		this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, this._commentForm, SimpleCommentEditor.getEditorOptions());
339
		const modeId = generateUuid() + '-' + (hasExistingComments ? this._commentThread.threadId : ++INMEM_MODEL_ID);
340 341 342 343 344
		const params = JSON.stringify({
			extensionId: this.extensionId,
			commentThreadId: this.commentThread.threadId
		});
		const resource = URI.parse(`${COMMENT_SCHEME}:commentinput-${modeId}.md?${params}`);
345
		const model = this.modelService.createModel(this._pendingComment || '', this.modeService.createByFilepathOrFirstLine(resource.path), resource, false);
346 347 348 349
		this._localToDispose.push(model);
		this._commentEditor.setModel(model);
		this._localToDispose.push(this._commentEditor);
		this._localToDispose.push(this._commentEditor.getModel().onDidChangeContent(() => this.setCommentEditorDecorations()));
P
Peng Lyu 已提交
350
		if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) {
351 352 353 354 355 356 357 358 359
			this._localToDispose.push(this._commentEditor.onDidFocusEditorWidget(() => {
				let commentThread = this._commentThread as modes.CommentThread2;
				commentThread.input = {
					uri: this._commentEditor.getModel().uri,
					value: this._commentEditor.getValue()
				};
				this.commentService.setActiveCommentThread(this._commentThread);
			}));

P
Peng Lyu 已提交
360
			this._localToDispose.push(this._commentEditor.getModel().onDidChangeContent(() => {
P
Peng Lyu 已提交
361
				let modelContent = this._commentEditor.getValue();
362 363 364 365 366
				let thread = (this._commentThread as modes.CommentThread2);
				if (thread.input.uri === this._commentEditor.getModel().uri && thread.input.value !== modelContent) {
					let newInput: modes.CommentInput = thread.input;
					newInput.value = modelContent;
					thread.input = newInput;
P
Peng Lyu 已提交
367
				}
P
Peng Lyu 已提交
368
			}));
P
Peng Lyu 已提交
369 370

			this._localToDispose.push((this._commentThread as modes.CommentThread2).onDidChangeInput(input => {
371 372 373 374 375
				let thread = (this._commentThread as modes.CommentThread2);

				if (thread.input.uri !== this._commentEditor.getModel().uri) {
					return;
				}
P
Peng Lyu 已提交
376

377 378 379 380
				if (this._commentEditor.getValue() !== input.value) {
					this._commentEditor.setValue(input.value);

					if (input.value === '') {
P
Peng Lyu 已提交
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
						this._pendingComment = '';
						if (dom.hasClass(this._commentForm, 'expand')) {
							dom.removeClass(this._commentForm, 'expand');
						}
						this._commentEditor.getDomNode().style.outline = '';
						this._error.textContent = '';
						dom.addClass(this._error, 'hidden');
					}
				}
			}));

			this._localToDispose.push((this._commentThread as modes.CommentThread2).onDidChangeComments(_ => {
				this.update(this._commentThread);
			}));
		}

397 398 399
		this.setCommentEditorDecorations();

		// Only add the additional step of clicking a reply button to expand the textarea when there are existing comments
400 401 402 403 404 405 406 407 408
		if (hasExistingComments) {
			this.createReplyButton();
		} else {
			if (!dom.hasClass(this._commentForm, 'expand')) {
				dom.addClass(this._commentForm, 'expand');
				this._commentEditor.focus();
			}
		}

409
		this._localToDispose.push(this._commentEditor.onKeyDown((ev: IKeyboardEvent) => {
410
			const hasExistingComments = this._commentThread.comments.length > 0;
411 412

			if (this._commentEditor.getModel().getValueLength() === 0 && ev.keyCode === KeyCode.Escape) {
P
Peng Lyu 已提交
413
				if (hasExistingComments || (this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) {
414 415 416 417 418
					if (dom.hasClass(this._commentForm, 'expand')) {
						dom.removeClass(this._commentForm, 'expand');
					}
				} else {
					this.dispose();
P
Peng Lyu 已提交
419
					return;
420 421
				}
			}
422

423
			if (this._commentEditor.getModel().getValueLength() !== 0 && ev.keyCode === KeyCode.Enter && (ev.ctrlKey || ev.metaKey)) {
424 425 426
				let lineNumber = this._commentGlyph.getPosition().position.lineNumber;
				this.createComment(lineNumber);
			}
427
		}));
428

429
		this._error = dom.append(this._commentForm, dom.$('.validation-error.hidden'));
430

431
		this._formActions = dom.append(this._commentForm, dom.$('.form-actions'));
P
Peng Lyu 已提交
432 433 434 435 436 437 438 439
		if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) {
			this.createCommentWidgetActions2(this._formActions, model);

			this._localToDispose.push((this._commentThread as modes.CommentThread2).onDidChangeAcceptInputCommands(_ => {
				dom.clearNode(this._formActions);
				this.createCommentWidgetActions2(this._formActions, model);
			}));
		} else {
P
Peng Lyu 已提交
440
			this.createCommentWidgetActions(this._formActions, model);
P
Peng Lyu 已提交
441
		}
R
rebornix 已提交
442

443 444 445 446 447 448 449 450 451 452 453 454 455 456
		this._resizeObserver = new MutationObserver(this._refresh.bind(this));

		this._resizeObserver.observe(this._bodyElement, {
			attributes: true,
			childList: true,
			characterData: true,
			subtree: true
		});

		if (this._commentThread.collapsibleState === modes.CommentThreadCollapsibleState.Expanded) {
			this.show({ lineNumber: lineNumber, column: 1 }, 2);
		}

		// If there are no existing comments, place focus on the text area. This must be done after show, which also moves focus.
P
Peng Lyu 已提交
457
		if ((this._commentThread as modes.CommentThread).reply && !this._commentThread.comments.length) {
458 459 460 461 462
			this._commentEditor.focus();
		} else if (this._commentEditor.getModel().getValueLength() > 0) {
			if (!dom.hasClass(this._commentForm, 'expand')) {
				dom.addClass(this._commentForm, 'expand');
			}
463 464 465 466
			this._commentEditor.focus();
		}
	}

467 468 469 470 471 472
	private handleError(e: Error) {
		this._error.textContent = e.message;
		this._commentEditor.getDomNode().style.outline = `1px solid ${this.themeService.getTheme().getColor(inputValidationErrorBorder)}`;
		dom.removeClass(this._error, 'hidden');
	}

R
rebornix 已提交
473 474
	private createCommentWidgetActions(container: HTMLElement, model: ITextModel) {
		const button = new Button(container);
475
		this._localToDispose.push(attachButtonStyler(button, this.themeService));
R
rebornix 已提交
476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497
		button.label = 'Add comment';

		button.enabled = model.getValueLength() > 0;
		this._localToDispose.push(this._commentEditor.onDidChangeModelContent(_ => {
			if (this._commentEditor.getValue()) {
				button.enabled = true;
			} else {
				button.enabled = false;
			}
		}));

		button.onDidClick(async () => {
			let lineNumber = this._commentGlyph.getPosition().position.lineNumber;
			this.createComment(lineNumber);
		});

		if (this._draftMode === modes.DraftMode.NotSupported) {
			return;
		}

		switch (this._draftMode) {
			case modes.DraftMode.InDraft:
R
rebornix 已提交
498 499 500
				const deleteDraftLabel = this.commentService.getDeleteDraftLabel(this._owner);
				if (deleteDraftLabel) {
					const deletedraftButton = new Button(container);
501
					this._disposables.push(attachButtonStyler(deletedraftButton, this.themeService));
R
rebornix 已提交
502 503 504
					deletedraftButton.label = deleteDraftLabel;
					deletedraftButton.enabled = true;

505
					this._disposables.push(deletedraftButton.onDidClick(async () => {
506
						try {
507
							await this.commentService.deleteDraft(this._owner, this.editor.getModel().uri);
508 509 510
						} catch (e) {
							this.handleError(e);
						}
511
					}));
R
rebornix 已提交
512
				}
R
rebornix 已提交
513

R
rebornix 已提交
514 515 516
				const submitDraftLabel = this.commentService.getFinishDraftLabel(this._owner);
				if (submitDraftLabel) {
					const submitdraftButton = new Button(container);
517
					this._disposables.push(attachButtonStyler(submitdraftButton, this.themeService));
R
rebornix 已提交
518 519 520 521
					submitdraftButton.label = this.commentService.getFinishDraftLabel(this._owner);
					submitdraftButton.enabled = true;

					submitdraftButton.onDidClick(async () => {
522 523
						try {
							let lineNumber = this._commentGlyph.getPosition().position.lineNumber;
524 525 526
							if (this._commentEditor.getValue()) {
								await this.createComment(lineNumber);
							}
527
							await this.commentService.finishDraft(this._owner, this.editor.getModel().uri);
528 529 530
						} catch (e) {
							this.handleError(e);
						}
R
rebornix 已提交
531 532
					});
				}
R
rebornix 已提交
533 534 535

				break;
			case modes.DraftMode.NotInDraft:
R
rebornix 已提交
536 537 538
				const startDraftLabel = this.commentService.getStartDraftLabel(this._owner);
				if (startDraftLabel) {
					const draftButton = new Button(container);
539
					this._disposables.push(attachButtonStyler(draftButton, this.themeService));
R
rebornix 已提交
540 541 542 543 544 545 546 547 548 549 550
					draftButton.label = this.commentService.getStartDraftLabel(this._owner);

					draftButton.enabled = model.getValueLength() > 0;
					this._localToDispose.push(this._commentEditor.onDidChangeModelContent(_ => {
						if (this._commentEditor.getValue()) {
							draftButton.enabled = true;
						} else {
							draftButton.enabled = false;
						}
					}));

551
					this._disposables.push(draftButton.onDidClick(async () => {
552
						try {
553
							await this.commentService.startDraft(this._owner, this.editor.getModel().uri);
554 555 556 557 558
							let lineNumber = this._commentGlyph.getPosition().position.lineNumber;
							await this.createComment(lineNumber);
						} catch (e) {
							this.handleError(e);
						}
559
					}));
R
rebornix 已提交
560
				}
R
rebornix 已提交
561 562 563 564 565

				break;
		}
	}

P
Peng Lyu 已提交
566 567 568 569 570 571 572 573 574 575 576 577 578 579
	/**
	 * Command based actions.
	 */
	private createCommentWidgetActions2(container: HTMLElement, model: ITextModel) {
		let commentThread = this._commentThread as modes.CommentThread2;

		commentThread.acceptInputCommands.reverse().forEach(command => {
			const button = new Button(container);
			this._localToDispose.push(attachButtonStyler(button, this.themeService));

			button.label = command.title;
			let commandId = command.id;
			let args = command.arguments || [];
			this._localToDispose.push(button.onDidClick(async () => {
580 581 582 583
				commentThread.input = {
					uri: this._commentEditor.getModel().uri,
					value: this._commentEditor.getValue()
				};
P
Peng Lyu 已提交
584 585 586 587 588 589
				this.commentService.setActiveCommentThread(this._commentThread);
				await this.commandService.executeCommand(commandId, ...args);
			}));
		});
	}

590 591
	private createNewCommentNode(comment: modes.Comment): CommentNode {
		let newCommentNode = new CommentNode(
P
Peng Lyu 已提交
592
			this._commentThread,
593 594 595 596 597 598 599
			comment,
			this.owner,
			this.editor.getModel().uri,
			this._markdownRenderer,
			this.themeService,
			this.instantiationService,
			this.commentService,
P
Peng Lyu 已提交
600
			this.commandService,
601 602
			this.modelService,
			this.modeService,
603
			this.dialogService,
P
Peng Lyu 已提交
604 605
			this.notificationService,
			this.contextMenuService);
606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630

		this._disposables.push(newCommentNode);
		this._disposables.push(newCommentNode.onDidDelete(deletedNode => {
			const deletedNodeId = deletedNode.comment.commentId;
			const deletedElementIndex = arrays.firstIndex(this._commentElements, commentNode => commentNode.comment.commentId === deletedNodeId);
			if (deletedElementIndex > -1) {
				this._commentElements.splice(deletedElementIndex, 1);
			}

			const deletedCommentIndex = arrays.firstIndex(this._commentThread.comments, comment => comment.commentId === deletedNodeId);
			if (deletedCommentIndex > -1) {
				this._commentThread.comments.splice(deletedCommentIndex, 1);
			}

			this._commentsElement.removeChild(deletedNode.domNode);
			deletedNode.dispose();

			if (this._commentThread.comments.length === 0) {
				this.dispose();
			}
		}));

		return newCommentNode;
	}

631 632
	private async createComment(lineNumber: number): Promise<void> {
		try {
633
			let newCommentThread;
634
			const isReply = this._commentThread.threadId !== null;
635

636
			if (isReply) {
637 638 639 640 641 642 643
				newCommentThread = await this.commentService.replyToCommentThread(
					this._owner,
					this.editor.getModel().uri,
					new Range(lineNumber, 1, lineNumber, 1),
					this._commentThread,
					this._commentEditor.getValue()
				);
644
			} else {
645
				newCommentThread = await this.commentService.createNewCommentThread(
646 647 648 649 650
					this._owner,
					this.editor.getModel().uri,
					new Range(lineNumber, 1, lineNumber, 1),
					this._commentEditor.getValue()
				);
651

652 653 654
				if (newCommentThread) {
					this.createReplyButton();
				}
655 656
			}

657
			if (newCommentThread) {
658
				this._commentEditor.setValue('');
659
				this._pendingComment = '';
660 661 662 663 664 665
				if (dom.hasClass(this._commentForm, 'expand')) {
					dom.removeClass(this._commentForm, 'expand');
				}
				this._commentEditor.getDomNode().style.outline = '';
				this._error.textContent = '';
				dom.addClass(this._error, 'hidden');
666
				this.update(newCommentThread);
667 668 669 670

				if (!isReply) {
					this._onDidCreateThread.fire(this);
				}
671
			}
672 673 674 675 676 677
		} catch (e) {
			this._error.textContent = e.message
				? nls.localize('commentCreationError', "Adding a comment failed: {0}.", e.message)
				: nls.localize('commentCreationDefaultError', "Adding a comment failed. Please try again or report an issue with the extension if the problem persists.");
			this._commentEditor.getDomNode().style.outline = `1px solid ${this.themeService.getTheme().getColor(inputValidationErrorBorder)}`;
			dom.removeClass(this._error, 'hidden');
678
		}
679 680
	}

681 682 683 684 685 686 687 688
	private createThreadLabel() {
		let label: string;
		if (this._commentThread.comments.length) {
			const participantsList = this._commentThread.comments.filter(arrays.uniqueFilter(comment => comment.userName)).map(comment => `@${comment.userName}`).join(', ');
			label = nls.localize('commentThreadParticipants', "Participants: {0}", participantsList);
		} else {
			label = nls.localize('startThread', "Start discussion");
		}
689

690
		this._headingLabel.innerHTML = strings.escape(label);
691
		this._headingLabel.setAttribute('aria-label', label);
692 693
	}

694 695 696 697 698 699 700
	private expandReplyArea() {
		if (!dom.hasClass(this._commentForm, 'expand')) {
			dom.addClass(this._commentForm, 'expand');
			this._commentEditor.focus();
		}
	}

701
	private createReplyButton() {
702
		this._reviewThreadReplyButton = <HTMLButtonElement>dom.append(this._commentForm, dom.$('button.review-thread-reply-button'));
P
Peng Lyu 已提交
703 704 705
		if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) {
			// this._reviewThreadReplyButton.title = (this._commentThread as modes.CommentThread2).acceptInputCommands.title;
		} else {
P
Peng Lyu 已提交
706
			this._reviewThreadReplyButton.title = nls.localize('reply', "Reply...");
P
Peng Lyu 已提交
707
		}
708
		this._reviewThreadReplyButton.textContent = nls.localize('reply', "Reply...");
709
		// bind click/escape actions for reviewThreadReplyButton and textArea
710 711
		this._localToDispose.push(dom.addDisposableListener(this._reviewThreadReplyButton, 'click', _ => this.expandReplyArea()));
		this._localToDispose.push(dom.addDisposableListener(this._reviewThreadReplyButton, 'focus', _ => this.expandReplyArea()));
712 713 714 715 716 717

		this._commentEditor.onDidBlurEditorWidget(() => {
			if (this._commentEditor.getModel().getValueLength() === 0 && dom.hasClass(this._commentForm, 'expand')) {
				dom.removeClass(this._commentForm, 'expand');
			}
		});
718 719
	}

720 721 722 723 724 725 726 727
	_refresh() {
		if (!this._isCollapsed && this._bodyElement) {
			let dimensions = dom.getClientArea(this._bodyElement);
			const headHeight = Math.ceil(this.editor.getConfiguration().lineHeight * 1.2);
			const lineHeight = this.editor.getConfiguration().lineHeight;
			const arrowHeight = Math.round(lineHeight / 3);
			const frameThickness = Math.round(lineHeight / 9) * 2;

728
			const computedLinesNumber = Math.ceil((headHeight + dimensions.height + arrowHeight + frameThickness + 8 /** margin bottom to avoid margin collapse */) / lineHeight);
729 730 731 732
			this._relayout(computedLinesNumber);
		}
	}

P
Peng Lyu 已提交
733
	private setCommentEditorDecorations() {
734 735
		const model = this._commentEditor && this._commentEditor.getModel();
		if (model) {
736 737
			let valueLength = model.getValueLength();
			const hasExistingComments = this._commentThread.comments.length > 0;
738
			let keybinding = platform.isMacintosh ? 'Cmd+Enter' : 'Ctrl+Enter';
739 740 741
			let placeholder = valueLength > 0
				? ''
				: (hasExistingComments
742 743
					? `Reply... (press ${keybinding} to submit)`
					: `Type a new comment (press ${keybinding} to submit)`);
744 745 746 747 748 749 750 751 752 753 754 755
			const decorations = [{
				range: {
					startLineNumber: 0,
					endLineNumber: 0,
					startColumn: 0,
					endColumn: 1
				},
				renderOptions: {
					after: {
						contentText: placeholder,
						color: transparent(editorForeground, 0.4)(this.themeService.getTheme()).toString()
					}
P
Peng Lyu 已提交
756
				}
757
			}];
P
Peng Lyu 已提交
758

759 760
			this._commentEditor.setDecorations(COMMENTEDITOR_DECORATION_KEY, decorations);
		}
P
Peng Lyu 已提交
761 762
	}

763
	private mouseDownInfo: { lineNumber: number };
764 765

	private onEditorMouseDown(e: IEditorMouseEvent): void {
766 767 768 769 770 771 772 773
		this.mouseDownInfo = null;

		const range = e.target.range;

		if (!range) {
			return;
		}

774 775 776 777
		if (!e.event.leftButton) {
			return;
		}

778
		if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) {
779 780 781
			return;
		}

782 783 784 785
		const data = e.target.detail as IMarginData;
		const gutterOffsetX = data.offsetX - data.glyphMarginWidth - data.lineNumbersWidth - data.glyphMarginLeft;

		// don't collide with folding and git decorations
786
		if (gutterOffsetX > 14) {
787
			return;
788 789
		}

790
		this.mouseDownInfo = { lineNumber: range.startLineNumber };
791 792 793 794 795 796 797
	}

	private onEditorMouseUp(e: IEditorMouseEvent): void {
		if (!this.mouseDownInfo) {
			return;
		}

798 799 800 801 802
		const { lineNumber } = this.mouseDownInfo;
		this.mouseDownInfo = null;

		const range = e.target.range;

803 804 805 806
		if (!range || range.startLineNumber !== lineNumber) {
			return;
		}

807 808 809 810 811 812 813 814
		if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) {
			return;
		}

		if (!e.target.element) {
			return;
		}

815
		if (this._commentGlyph && this._commentGlyph.getPosition().position.lineNumber !== lineNumber) {
816 817 818
			return;
		}

819 820 821 822 823
		if (e.target.element.className.indexOf('comment-thread') >= 0) {
			if (this._isCollapsed) {
				this.show({ lineNumber: lineNumber, column: 1 }, 2);
			} else {
				this.hide();
824
				if (this._commentThread === null || this._commentThread.threadId === null) {
825 826
					this.dispose();
				}
827 828 829 830 831
			}
		}
	}

	private _applyTheme(theme: ITheme) {
M
Matt Bierner 已提交
832
		const borderColor = theme.getColor(peekViewBorder) || Color.transparent;
833 834 835 836
		this.style({
			arrowColor: borderColor,
			frameColor: borderColor
		});
837 838 839 840

		const content: string[] = [];
		const linkColor = theme.getColor(textLinkForeground);
		if (linkColor) {
841
			content.push(`.monaco-editor .review-widget .body .comment-body a { color: ${linkColor} }`);
842 843 844 845
		}

		const linkActiveColor = theme.getColor(textLinkActiveForeground);
		if (linkActiveColor) {
846
			content.push(`.monaco-editor .review-widget .body .comment-body a:hover, a:active { color: ${linkActiveColor} }`);
847 848 849 850
		}

		const focusColor = theme.getColor(focusBorder);
		if (focusColor) {
851 852
			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}; }`);
853 854
		}

855 856 857 858 859 860 861 862 863 864
		const blockQuoteBackground = theme.getColor(textBlockQuoteBackground);
		if (blockQuoteBackground) {
			content.push(`.monaco-editor .review-widget .body .review-comment blockquote { background: ${blockQuoteBackground}; }`);
		}

		const blockQuoteBOrder = theme.getColor(textBlockQuoteBorder);
		if (blockQuoteBOrder) {
			content.push(`.monaco-editor .review-widget .body .review-comment blockquote { border-color: ${blockQuoteBOrder}; }`);
		}

865 866 867
		const hcBorder = theme.getColor(contrastBorder);
		if (hcBorder) {
			content.push(`.monaco-editor .review-widget .body .comment-form .review-thread-reply-button { outline-color: ${hcBorder}; }`);
868
			content.push(`.monaco-editor .review-widget .body .monaco-editor { outline: 1px solid ${hcBorder}; }`);
869 870
		}

871 872
		const errorBorder = theme.getColor(inputValidationErrorBorder);
		if (errorBorder) {
873
			content.push(`.monaco-editor .review-widget .validation-error { border: 1px solid ${errorBorder}; }`);
874 875 876 877
		}

		const errorBackground = theme.getColor(inputValidationErrorBackground);
		if (errorBackground) {
878
			content.push(`.monaco-editor .review-widget .validation-error { background: ${errorBackground}; }`);
879 880 881 882 883
		}

		const errorForeground = theme.getColor(inputValidationErrorForeground);
		if (errorForeground) {
			content.push(`.monaco-editor .review-widget .body .comment-form .validation-error { color: ${errorForeground}; }`);
884 885
		}

886 887 888 889 890 891 892
		const fontInfo = this.editor.getConfiguration().fontInfo;
		content.push(`.monaco-editor .review-widget .body code {
			font-family: ${fontInfo.fontFamily};
			font-size: ${fontInfo.fontSize}px;
			font-weight: ${fontInfo.fontWeight};
		}`);

893
		this._styleElement.innerHTML = content.join('\n');
894 895 896

		// Editor decorations should also be responsive to theme changes
		this.setCommentEditorDecorations();
897 898
	}

899 900 901
	show(rangeOrPos: IRange | IPosition, heightInLines: number): void {
		this._isCollapsed = false;
		super.show(rangeOrPos, heightInLines);
902
		this._refresh();
903 904 905 906 907 908 909
	}

	hide() {
		this._isCollapsed = true;
		super.hide();
	}

910 911 912 913 914 915
	dispose() {
		super.dispose();
		if (this._resizeObserver) {
			this._resizeObserver.disconnect();
			this._resizeObserver = null;
		}
916

917
		if (this._commentGlyph) {
918
			this._commentGlyph.dispose();
919 920
			this._commentGlyph = null;
		}
921

922
		this._globalToDispose.forEach(global => global.dispose());
923
		this._localToDispose.forEach(local => local.dispose());
924
		this._onDidClose.fire(undefined);
925 926
	}
}