commentThreadWidget.ts 32.6 KB
Newer Older
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as dom from 'vs/base/browser/dom';
P
Peng Lyu 已提交
7
import { ActionBar, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
P
Peng Lyu 已提交
8
import { Action, IAction } from 'vs/base/common/actions';
9 10 11
import * as arrays from 'vs/base/common/arrays';
import { Color } from 'vs/base/common/color';
import { Emitter, Event } from 'vs/base/common/event';
12
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
13
import * as strings from 'vs/base/common/strings';
14 15 16 17
import { withNullAsUndefined } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import { IMarginData } from 'vs/editor/browser/controller/mouseTarget';
18
import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser';
19 20 21
import { IPosition } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { ITextModel } from 'vs/editor/common/model';
22
import * as modes from 'vs/editor/common/modes';
P
Peng Lyu 已提交
23 24
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
25
import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer';
26 27 28 29 30 31 32 33
import { peekViewBorder } from 'vs/editor/contrib/referenceSearch/referencesWidget';
import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget';
import * as nls from 'vs/nls';
import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IMenu, MenuItemAction } from 'vs/platform/actions/common/actions';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
P
Peng Lyu 已提交
34 35
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { INotificationService } from 'vs/platform/notification/common/notification';
36 37 38
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { contrastBorder, editorForeground, focusBorder, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, transparent } from 'vs/platform/theme/common/colorRegistry';
import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService';
39
import { CommentFormActions } from 'vs/workbench/contrib/comments/browser/commentFormActions';
40 41 42 43 44 45 46
import { CommentGlyphWidget } from 'vs/workbench/contrib/comments/browser/commentGlyphWidget';
import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus';
import { CommentNode } from 'vs/workbench/contrib/comments/browser/commentNode';
import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService';
import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys';
import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget';
import { SimpleCommentEditor } from './simpleCommentEditor';
47

P
Peng Lyu 已提交
48
export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration';
M
Miguel Solorio 已提交
49
const COLLAPSE_ACTION_CLASS = 'expand-review-action';
P
Peng Lyu 已提交
50
const COMMENT_SCHEME = 'comment';
51

52

53
let INMEM_MODEL_ID = 0;
54

55
export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget {
56
	private _headElement: HTMLElement;
57
	protected _headingLabel: HTMLElement;
58 59
	protected _actionbarWidget: ActionBar;
	private _bodyElement: HTMLElement;
60
	private _parentEditor: ICodeEditor;
P
Peng Lyu 已提交
61
	private _commentEditor: ICodeEditor;
62 63
	private _commentsElement: HTMLElement;
	private _commentElements: CommentNode[];
64 65
	private _commentForm: HTMLElement;
	private _reviewThreadReplyButton: HTMLElement;
66
	private _resizeObserver: any;
67
	private _onDidClose = new Emitter<ReviewZoneWidget | undefined>();
68
	private _onDidCreateThread = new Emitter<ReviewZoneWidget>();
69
	private _isExpanded?: boolean;
70
	private _collapseAction: Action;
71
	private _commentGlyph?: CommentGlyphWidget;
72
	private _submitActionsDisposables: IDisposable[];
73
	private readonly _globalToDispose = new DisposableStore();
P
Peng Lyu 已提交
74
	private _commentThreadDisposables: IDisposable[] = [];
75 76
	private _markdownRenderer: MarkdownRenderer;
	private _styleElement: HTMLStyleElement;
77
	private _formActions: HTMLElement | null;
78
	private _error: HTMLElement;
79 80
	private _contextKeyService: IContextKeyService;
	private _threadIsEmpty: IContextKey<boolean>;
P
Peng Lyu 已提交
81
	private _commentThreadContextValue: IContextKey<string>;
82
	private _commentEditorIsEmpty: IContextKey<boolean>;
83
	private _commentFormActions: CommentFormActions;
84

85
	public get owner(): string {
86 87
		return this._owner;
	}
88
	public get commentThread(): modes.CommentThread {
89
		return this._commentThread;
90
	}
91

M
Matt Bierner 已提交
92
	public get extensionId(): string | undefined {
93 94 95
		return this._commentThread.extensionId;
	}

P
Peng Lyu 已提交
96 97
	private _commentMenus: CommentMenus;

98 99
	constructor(
		editor: ICodeEditor,
100
		private _owner: string,
101
		private _commentThread: modes.CommentThread,
102 103 104 105 106 107
		private _pendingComment: string,
		@IInstantiationService private instantiationService: IInstantiationService,
		@IModeService private modeService: IModeService,
		@IModelService private modelService: IModelService,
		@IThemeService private themeService: IThemeService,
		@ICommentService private commentService: ICommentService,
P
Peng Lyu 已提交
108 109 110
		@IOpenerService private openerService: IOpenerService,
		@IKeybindingService private keybindingService: IKeybindingService,
		@INotificationService private notificationService: INotificationService,
111 112
		@IContextMenuService private contextMenuService: IContextMenuService,
		@IContextKeyService contextKeyService: IContextKeyService
113
	) {
114
		super(editor, { keepEditorSelection: true });
115 116 117
		this._contextKeyService = contextKeyService.createScoped(this.domNode);
		this._threadIsEmpty = CommentContextKeys.commentThreadIsEmpty.bindTo(this._contextKeyService);
		this._threadIsEmpty.set(!_commentThread.comments || !_commentThread.comments.length);
P
Peng Lyu 已提交
118
		this._commentThreadContextValue = contextKeyService.createKey('commentThread', _commentThread.contextValue);
119

120
		this._resizeObserver = null;
121
		this._isExpanded = _commentThread.collapsibleState === modes.CommentThreadCollapsibleState.Expanded;
P
Peng Lyu 已提交
122
		this._commentThreadDisposables = [];
123
		this._submitActionsDisposables = [];
124
		this._formActions = null;
P
Peng Lyu 已提交
125
		this._commentMenus = this.commentService.getCommentMenus(this._owner);
126
		this.create();
127 128

		this._styleElement = dom.createStyleSheet(this.domNode);
129 130
		this._globalToDispose.add(this.themeService.onThemeChange(this._applyTheme, this));
		this._globalToDispose.add(this.editor.onDidChangeConfiguration(e => {
131 132 133 134
			if (e.fontInfo) {
				this._applyTheme(this.themeService.getTheme());
			}
		}));
135 136
		this._applyTheme(this.themeService.getTheme());

137
		this._markdownRenderer = this._globalToDispose.add(new MarkdownRenderer(editor, this.modeService, this.openerService));
138
		this._parentEditor = editor;
139 140
	}

M
Matt Bierner 已提交
141
	public get onDidClose(): Event<ReviewZoneWidget | undefined> {
142 143 144
		return this._onDidClose.event;
	}

145 146 147 148
	public get onDidCreateThread(): Event<ReviewZoneWidget> {
		return this._onDidCreateThread.event;
	}

149
	public getPosition(): IPosition | undefined {
M
Matt Bierner 已提交
150 151
		if (this.position) {
			return this.position;
152 153
		}

M
Matt Bierner 已提交
154 155 156 157
		if (this._commentGlyph) {
			return withNullAsUndefined(this._commentGlyph.getPosition().position);
		}
		return undefined;
158 159
	}

P
Peng Lyu 已提交
160 161 162 163
	protected revealLine(lineNumber: number) {
		// we don't do anything here as we always do the reveal ourselves.
	}

P
Peng Lyu 已提交
164
	public reveal(commentUniqueId?: number) {
165
		if (!this._isExpanded) {
166
			this.show({ lineNumber: this._commentThread.range.startLineNumber, column: 1 }, 2);
167
		}
168

P
Peng Lyu 已提交
169
		if (commentUniqueId !== undefined) {
P
Peng Lyu 已提交
170
			let height = this.editor.getLayoutInfo().height;
P
Peng Lyu 已提交
171
			let matchedNode = this._commentElements.filter(commentNode => commentNode.comment.uniqueIdInThread === commentUniqueId);
P
Peng Lyu 已提交
172 173 174 175 176 177 178 179 180
			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;
			}
		}

181
		this.editor.revealRangeInCenter(this._commentThread.range);
182 183
	}

184
	public getPendingComment(): string | null {
185 186 187
		if (this._commentEditor) {
			let model = this._commentEditor.getModel();

188
			if (model && model.getValueLength() > 0) { // checking length is cheap
189 190 191 192 193 194 195
				return model.getValue();
			}
		}

		return null;
	}

196 197
	protected _fillContainer(container: HTMLElement): void {
		this.setCssClass('review-widget');
198
		this._headElement = <HTMLDivElement>dom.$('.head');
199 200 201
		container.appendChild(this._headElement);
		this._fillHead(this._headElement);

202
		this._bodyElement = <HTMLDivElement>dom.$('.body');
203
		container.appendChild(this._bodyElement);
204 205 206 207

		dom.addDisposableListener(this._bodyElement, dom.EventType.FOCUS_IN, e => {
			this.commentService.setActiveCommentThread(this._commentThread);
		});
208 209 210
	}

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

213
		this._headingLabel = dom.append(titleElement, dom.$('span.filename'));
214
		this.createThreadLabel();
215

216
		const actionsContainer = dom.append(this._headElement, dom.$('.review-actions'));
P
Peng Lyu 已提交
217 218 219
		this._actionbarWidget = new ActionBar(actionsContainer, {
			actionViewItemProvider: (action: IAction) => {
				if (action instanceof MenuItemAction) {
P
Peng Lyu 已提交
220
					let item = new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService);
P
Peng Lyu 已提交
221 222 223 224 225 226 227 228
					return item;
				} else {
					let item = new ActionViewItem({}, action, { label: false, icon: true });
					return item;
				}
			}
		});

M
Matt Bierner 已提交
229
		this._disposables.add(this._actionbarWidget);
230

231 232
		this._collapseAction = new Action('review.expand', nls.localize('label.collapse', "Collapse"), COLLAPSE_ACTION_CLASS, true, () => this.collapse());

233 234
		const menu = this._commentMenus.getCommentThreadTitleActions(this._commentThread, this._contextKeyService);
		this.setActionBarActions(menu);
235

236 237 238 239
		this._disposables.add(menu);
		this._disposables.add(menu.onDidChange(e => {
			this.setActionBarActions(menu);
		}));
P
Peng Lyu 已提交
240 241

		this._actionbarWidget.context = this._commentThread;
242 243
	}

244 245
	private setActionBarActions(menu: IMenu): void {
		const groups = menu.getActions({ shouldForwardArgs: true }).reduce((r, [, actions]) => [...r, ...actions], <MenuItemAction[]>[]);
246
		this._actionbarWidget.clear();
247 248 249
		this._actionbarWidget.push([...groups, this._collapseAction], { label: false, icon: true });
	}

250 251 252 253 254
	private deleteCommentThread(): void {
		this.dispose();
		this.commentService.disposeCommentThread(this.owner, this._commentThread.threadId);
	}

255
	public collapse(): Promise<void> {
256
		if (this._commentThread.comments && this._commentThread.comments.length === 0) {
257
			this.deleteCommentThread();
258
			return Promise.resolve();
259
		}
260

261 262
		this.hide();
		return Promise.resolve();
263 264
	}

265
	public getGlyphPosition(): number {
M
Matt Bierner 已提交
266 267 268 269
		if (this._commentGlyph) {
			return this._commentGlyph.getPosition().position!.lineNumber;
		}
		return 0;
270 271 272
	}

	toggleExpand(lineNumber: number) {
273
		if (this._isExpanded) {
274
			this.hide();
275
			if (!this._commentThread.comments || !this._commentThread.comments.length) {
276
				this.deleteCommentThread();
277
			}
278 279
		} else {
			this.show({ lineNumber: lineNumber, column: 1 }, 2);
280
		}
281 282
	}

283
	async update(commentThread: modes.CommentThread) {
284
		const oldCommentsLen = this._commentElements.length;
285
		const newCommentsLen = commentThread.comments ? commentThread.comments.length : 0;
286
		this._threadIsEmpty.set(!newCommentsLen);
287 288 289 290 291

		let commentElementsToDel: CommentNode[] = [];
		let commentElementsToDelIndex: number[] = [];
		for (let i = 0; i < oldCommentsLen; i++) {
			let comment = this._commentElements[i].comment;
P
Peng Lyu 已提交
292
			let newComment = commentThread.comments ? commentThread.comments.filter(c => c.uniqueIdInThread === comment.uniqueIdInThread) : [];
293 294 295 296 297 298 299 300 301 302 303

			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 已提交
304
			this._commentElements.splice(commentElementsToDelIndex[i], 1);
305 306 307
			this._commentsElement.removeChild(commentElementsToDel[i].domNode);
		}

308
		let lastCommentElement: HTMLElement | null = null;
309 310
		let newCommentNodeList: CommentNode[] = [];
		for (let i = newCommentsLen - 1; i >= 0; i--) {
311
			let currentComment = commentThread.comments![i];
P
Peng Lyu 已提交
312
			let oldCommentNode = this._commentElements.filter(commentNode => commentNode.comment.uniqueIdInThread === currentComment.uniqueIdInThread);
313
			if (oldCommentNode.length) {
R
rebornix 已提交
314
				oldCommentNode[0].update(currentComment);
315 316 317
				lastCommentElement = oldCommentNode[0].domNode;
				newCommentNodeList.unshift(oldCommentNode[0]);
			} else {
318
				const newElement = this.createNewCommentNode(currentComment);
319

320 321 322 323 324 325 326 327 328 329 330 331 332
				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;
P
Peng Lyu 已提交
333
		this.createThreadLabel();
I
Ilya Biryukov 已提交
334

335 336 337
		if (this._formActions && this._commentEditor.hasModel()) {
			dom.clearNode(this._formActions);
			const model = this._commentEditor.getModel();
338
			this.createCommentWidgetActions(this._formActions, model);
339 340
		}

I
Ilya Biryukov 已提交
341 342
		// Move comment glyph widget and show position if the line has changed.
		const lineNumber = this._commentThread.range.startLineNumber;
343
		let shouldMoveWidget = false;
M
Matt Bierner 已提交
344 345
		if (this._commentGlyph) {
			if (this._commentGlyph.getPosition().position!.lineNumber !== lineNumber) {
346
				shouldMoveWidget = true;
M
Matt Bierner 已提交
347 348
				this._commentGlyph.setLineNumber(lineNumber);
			}
349 350
		}

P
Peng Lyu 已提交
351 352 353 354
		if (!this._reviewThreadReplyButton) {
			this.createReplyButton();
		}

355 356 357 358 359
		if (this._commentThread.comments && this._commentThread.comments.length === 0) {
			this.expandReplyArea();
		}

		if (shouldMoveWidget && this._isExpanded) {
360
			this.show({ lineNumber, column: 1 }, 2);
I
Ilya Biryukov 已提交
361
		}
362

363 364 365 366
		if (this._commentThread.collapsibleState === modes.CommentThreadCollapsibleState.Expanded) {
			this.show({ lineNumber, column: 1 }, 2);
		} else {
			this.hide();
367
		}
P
Peng Lyu 已提交
368 369 370 371 372 373

		if (this._commentThread.contextValue) {
			this._commentThreadContextValue.set(this._commentThread.contextValue);
		} else {
			this._commentThreadContextValue.reset();
		}
374 375
	}

376
	protected _onWidth(widthInPixel: number): void {
P
Peng Lyu 已提交
377
		this._commentEditor.layout({ height: 5 * 18, width: widthInPixel - 54 /* margin 20px * 10 + scrollbar 14px*/ });
378 379
	}

P
Peng Lyu 已提交
380
	protected _doLayout(heightInPixel: number, widthInPixel: number): void {
P
Peng Lyu 已提交
381
		this._commentEditor.layout({ height: 5 * 18, width: widthInPixel - 54 /* margin 20px * 10 + scrollbar 14px*/ });
P
Peng Lyu 已提交
382 383
	}

P
Peng Lyu 已提交
384
	display(lineNumber: number) {
385
		this._commentGlyph = new CommentGlyphWidget(this.editor, lineNumber);
386

M
Matt Bierner 已提交
387 388
		this._disposables.add(this.editor.onMouseDown(e => this.onEditorMouseDown(e)));
		this._disposables.add(this.editor.onMouseUp(e => this.onEditorMouseUp(e)));
389

M
Matt Bierner 已提交
390
		let headHeight = Math.ceil(this.editor.getConfiguration().lineHeight * 1.2);
391 392 393
		this._headElement.style.height = `${headHeight}px`;
		this._headElement.style.lineHeight = this._headElement.style.height;

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

397
		this._commentElements = [];
398 399 400
		if (this._commentThread.comments) {
			for (const comment of this._commentThread.comments) {
				const newCommentNode = this.createNewCommentNode(comment);
401

402 403 404
				this._commentElements.push(newCommentNode);
				this._commentsElement.appendChild(newCommentNode.domNode);
			}
405 406
		}

407
		const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0;
408
		this._commentForm = dom.append(this._bodyElement, dom.$('.comment-form'));
409
		this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, this._commentForm, SimpleCommentEditor.getEditorOptions(), this._parentEditor, this);
410 411
		this._commentEditorIsEmpty = CommentContextKeys.commentIsEmpty.bindTo(this._contextKeyService);
		this._commentEditorIsEmpty.set(!this._pendingComment);
412

413
		const modeId = generateUuid() + '-' + (hasExistingComments ? this._commentThread.threadId : ++INMEM_MODEL_ID);
414 415 416 417
		const params = JSON.stringify({
			extensionId: this.extensionId,
			commentThreadId: this.commentThread.threadId
		});
P
Peng Lyu 已提交
418 419 420 421 422 423 424

		let resource = URI.parse(`${COMMENT_SCHEME}://${this.extensionId}/commentinput-${modeId}.md?${params}`); // TODO. Remove params once extensions adopt authority.
		let commentController = this.commentService.getCommentController(this.owner);
		if (commentController) {
			resource = resource.with({ authority: commentController.id });
		}

B
Benjamin Pasero 已提交
425
		const model = this.modelService.createModel(this._pendingComment || '', this.modeService.createByFilepathOrFirstLine(resource), resource, false);
M
Matt Bierner 已提交
426
		this._disposables.add(model);
427
		this._commentEditor.setModel(model);
M
Matt Bierner 已提交
428 429
		this._disposables.add(this._commentEditor);
		this._disposables.add(this._commentEditor.getModel()!.onDidChangeContent(() => {
430 431 432 433
			this.setCommentEditorDecorations();
			this._commentEditorIsEmpty.set(!this._commentEditor.getValue());
		}));

434
		this.createTextModelListener();
P
Peng Lyu 已提交
435

436 437 438
		this.setCommentEditorDecorations();

		// Only add the additional step of clicking a reply button to expand the textarea when there are existing comments
439 440 441
		if (hasExistingComments) {
			this.createReplyButton();
		} else {
442 443
			if (this._commentThread.comments && this._commentThread.comments.length === 0) {
				this.expandReplyArea();
444 445 446
			}
		}

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

449
		this._formActions = dom.append(this._commentForm, dom.$('.form-actions'));
450 451
		this.createCommentWidgetActions(this._formActions, model);
		this.createCommentWidgetActionsListener();
R
rebornix 已提交
452

453 454 455 456 457 458 459 460 461 462 463 464 465 466
		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.
467
		// if this._commentThread.comments is undefined, it doesn't finish initialization yet, so we don't focus the editor immediately.
468
		if (this._commentThread.comments && !this._commentThread.comments.length) {
469
			this._commentEditor.focus();
470
		} else if (this._commentEditor.getModel()!.getValueLength() > 0) {
471
			this.expandReplyArea();
472 473 474
		}
	}

P
Peng Lyu 已提交
475
	private createTextModelListener() {
P
Peng Lyu 已提交
476
		this._commentThreadDisposables.push(this._commentEditor.onDidFocusEditorWidget(() => {
477
			this._commentThread.input = {
P
Peng Lyu 已提交
478 479 480
				uri: this._commentEditor.getModel()!.uri,
				value: this._commentEditor.getValue()
			};
481
			this.commentService.setActiveCommentThread(this._commentThread);
P
Peng Lyu 已提交
482 483
		}));

P
Peng Lyu 已提交
484
		this._commentThreadDisposables.push(this._commentEditor.getModel()!.onDidChangeContent(() => {
P
Peng Lyu 已提交
485
			let modelContent = this._commentEditor.getValue();
486 487
			if (this._commentThread.input && this._commentThread.input.uri === this._commentEditor.getModel()!.uri && this._commentThread.input.value !== modelContent) {
				let newInput: modes.CommentInput = this._commentThread.input;
P
Peng Lyu 已提交
488
				newInput.value = modelContent;
489
				this._commentThread.input = newInput;
P
Peng Lyu 已提交
490
			}
491
			this.commentService.setActiveCommentThread(this._commentThread);
P
Peng Lyu 已提交
492 493
		}));

494 495
		this._commentThreadDisposables.push(this._commentThread.onDidChangeInput(input => {
			let thread = this._commentThread;
P
Peng Lyu 已提交
496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518

			if (thread.input && thread.input.uri !== this._commentEditor.getModel()!.uri) {
				return;
			}
			if (!input) {
				return;
			}

			if (this._commentEditor.getValue() !== input.value) {
				this._commentEditor.setValue(input.value);

				if (input.value === '') {
					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');
				}
			}
		}));

519
		this._commentThreadDisposables.push(this._commentThread.onDidChangeComments(async _ => {
P
Peng Lyu 已提交
520 521 522
			await this.update(this._commentThread);
		}));

523
		this._commentThreadDisposables.push(this._commentThread.onDidChangeLabel(_ => {
P
Peng Lyu 已提交
524 525 526 527
			this.createThreadLabel();
		}));
	}

528 529
	private createCommentWidgetActionsListener() {
		this._commentThreadDisposables.push(this._commentThread.onDidChangeRange(range => {
P
Peng Lyu 已提交
530 531 532 533 534 535 536 537 538 539 540 541 542 543 544
			// Move comment glyph widget and show position if the line has changed.
			const lineNumber = this._commentThread.range.startLineNumber;
			let shouldMoveWidget = false;
			if (this._commentGlyph) {
				if (this._commentGlyph.getPosition().position!.lineNumber !== lineNumber) {
					shouldMoveWidget = true;
					this._commentGlyph.setLineNumber(lineNumber);
				}
			}

			if (shouldMoveWidget && this._isExpanded) {
				this.show({ lineNumber, column: 1 }, 2);
			}
		}));

545
		this._commentThreadDisposables.push(this._commentThread.onDidChangeCollasibleState(state => {
P
Peng Lyu 已提交
546 547 548 549 550 551 552 553 554 555 556 557 558 559
			if (state === modes.CommentThreadCollapsibleState.Expanded && !this._isExpanded) {
				const lineNumber = this._commentThread.range.startLineNumber;

				this.show({ lineNumber, column: 1 }, 2);
				return;
			}

			if (state === modes.CommentThreadCollapsibleState.Collapsed && this._isExpanded) {
				this.hide();
				return;
			}
		}));
	}

560 561 562 563
	private getActiveComment(): CommentNode | ReviewZoneWidget {
		return this._commentElements.filter(node => node.isEditing)[0] || this;
	}

P
Peng Lyu 已提交
564 565 566
	/**
	 * Command based actions.
	 */
567 568
	private createCommentWidgetActions(container: HTMLElement, model: ITextModel) {
		const commentThread = this._commentThread;
P
Peng Lyu 已提交
569

570
		const menu = this._commentMenus.getCommentThreadActions(commentThread, this._contextKeyService);
P
Peng Lyu 已提交
571

M
Matt Bierner 已提交
572 573
		this._disposables.add(menu);
		this._disposables.add(menu.onDidChange(() => {
574
			this._commentFormActions.setActions(menu);
575
		}));
P
Peng Lyu 已提交
576

577 578 579 580 581 582 583 584
		this._commentFormActions = new CommentFormActions(container, async (action: IAction) => {
			if (!commentThread.comments || !commentThread.comments.length) {
				let newPosition = this.getPosition();

				if (newPosition) {
					this.commentService.updateCommentThreadTemplate(this.owner, commentThread.commentThreadHandle, new Range(newPosition.lineNumber, 1, newPosition.lineNumber, 1));
				}
			}
585 586 587 588 589
			action.run({
				thread: this._commentThread,
				text: this._commentEditor.getValue(),
				$mid: 8
			});
P
Peng Lyu 已提交
590

591 592
			this.hideReplyArea();
		}, this.themeService);
593

594
		this._commentFormActions.setActions(menu);
P
Peng Lyu 已提交
595 596
	}

597
	private createNewCommentNode(comment: modes.Comment): CommentNode {
598
		let newCommentNode = this.instantiationService.createInstance(CommentNode,
P
Peng Lyu 已提交
599
			this._commentThread,
600 601
			comment,
			this.owner,
602
			this.editor.getModel()!.uri,
603 604 605
			this._parentEditor,
			this,
			this._markdownRenderer);
606

M
Matt Bierner 已提交
607 608
		this._disposables.add(newCommentNode);
		this._disposables.add(newCommentNode.onDidDelete(deletedNode => {
P
Peng Lyu 已提交
609 610
			const deletedNodeId = deletedNode.comment.uniqueIdInThread;
			const deletedElementIndex = arrays.firstIndex(this._commentElements, commentNode => commentNode.comment.uniqueIdInThread === deletedNodeId);
611 612 613 614
			if (deletedElementIndex > -1) {
				this._commentElements.splice(deletedElementIndex, 1);
			}

P
Peng Lyu 已提交
615
			const deletedCommentIndex = arrays.firstIndex(this._commentThread.comments!, comment => comment.uniqueIdInThread === deletedNodeId);
616
			if (deletedCommentIndex > -1) {
617
				this._commentThread.comments!.splice(deletedCommentIndex, 1);
618 619 620 621 622
			}

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

623
			if (this._commentThread.comments!.length === 0) {
624 625 626 627 628 629 630
				this.dispose();
			}
		}));

		return newCommentNode;
	}

631 632 633
	async submitComment(): Promise<void> {
		const activeComment = this.getActiveComment();
		if (activeComment instanceof ReviewZoneWidget) {
634 635
			if (this._commentFormActions) {
				this._commentFormActions.triggerDefaultAction();
636
			}
637
		}
638 639
	}

P
Peng Lyu 已提交
640
	private createThreadLabel() {
641
		let label: string | undefined;
642
		label = this._commentThread.label;
643

P
Peng Lyu 已提交
644
		if (label === undefined) {
645
			if (this._commentThread.comments && this._commentThread.comments.length) {
646 647 648 649 650
				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");
			}
651
		}
652

P
Peng Lyu 已提交
653 654 655 656
		if (label) {
			this._headingLabel.innerHTML = strings.escape(label);
			this._headingLabel.setAttribute('aria-label', label);
		}
657 658
	}

659 660 661 662 663 664 665
	private expandReplyArea() {
		if (!dom.hasClass(this._commentForm, 'expand')) {
			dom.addClass(this._commentForm, 'expand');
			this._commentEditor.focus();
		}
	}

P
Peng Lyu 已提交
666 667 668 669 670 671 672 673 674 675 676
	private hideReplyArea() {
		this._commentEditor.setValue('');
		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');
	}

677
	private createReplyButton() {
678
		this._reviewThreadReplyButton = <HTMLButtonElement>dom.append(this._commentForm, dom.$('button.review-thread-reply-button'));
679 680
		this._reviewThreadReplyButton.title = nls.localize('reply', "Reply...");

681
		this._reviewThreadReplyButton.textContent = nls.localize('reply', "Reply...");
682
		// bind click/escape actions for reviewThreadReplyButton and textArea
M
Matt Bierner 已提交
683 684
		this._disposables.add(dom.addDisposableListener(this._reviewThreadReplyButton, 'click', _ => this.expandReplyArea()));
		this._disposables.add(dom.addDisposableListener(this._reviewThreadReplyButton, 'focus', _ => this.expandReplyArea()));
685 686

		this._commentEditor.onDidBlurEditorWidget(() => {
687
			if (this._commentEditor.getModel()!.getValueLength() === 0 && dom.hasClass(this._commentForm, 'expand')) {
688 689 690
				dom.removeClass(this._commentForm, 'expand');
			}
		});
691 692
	}

693
	_refresh() {
694
		if (this._isExpanded && this._bodyElement) {
695 696 697 698 699 700
			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;

701
			const computedLinesNumber = Math.ceil((headHeight + dimensions.height + arrowHeight + frameThickness + 8 /** margin bottom to avoid margin collapse */) / lineHeight);
702 703 704 705 706 707 708

			let currentPosition = this.getPosition();

			if (this._viewZone && currentPosition && currentPosition.lineNumber !== this._viewZone.afterLineNumber) {
				this._viewZone.afterLineNumber = currentPosition.lineNumber;
			}

709 710 711 712
			this._relayout(computedLinesNumber);
		}
	}

P
Peng Lyu 已提交
713
	private setCommentEditorDecorations() {
714 715
		const model = this._commentEditor && this._commentEditor.getModel();
		if (model) {
716
			const valueLength = model.getValueLength();
717
			const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0;
718
			const placeholder = valueLength > 0
719
				? ''
720 721 722
				: hasExistingComments
					? nls.localize('reply', "Reply...")
					: nls.localize('newComment', "Type a new comment");
723 724 725 726 727 728 729 730 731 732
			const decorations = [{
				range: {
					startLineNumber: 0,
					endLineNumber: 0,
					startColumn: 0,
					endColumn: 1
				},
				renderOptions: {
					after: {
						contentText: placeholder,
733
						color: `${transparent(editorForeground, 0.4)(this.themeService.getTheme())}`
734
					}
P
Peng Lyu 已提交
735
				}
736
			}];
P
Peng Lyu 已提交
737

738 739
			this._commentEditor.setDecorations(COMMENTEDITOR_DECORATION_KEY, decorations);
		}
P
Peng Lyu 已提交
740 741
	}

742
	private mouseDownInfo: { lineNumber: number } | null;
743 744

	private onEditorMouseDown(e: IEditorMouseEvent): void {
745 746 747 748 749 750 751 752
		this.mouseDownInfo = null;

		const range = e.target.range;

		if (!range) {
			return;
		}

753 754 755 756
		if (!e.event.leftButton) {
			return;
		}

757
		if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) {
758 759 760
			return;
		}

761 762 763 764
		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
765
		if (gutterOffsetX > 14) {
766
			return;
767 768
		}

769
		this.mouseDownInfo = { lineNumber: range.startLineNumber };
770 771 772 773 774 775 776
	}

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

777 778 779 780 781
		const { lineNumber } = this.mouseDownInfo;
		this.mouseDownInfo = null;

		const range = e.target.range;

782 783 784 785
		if (!range || range.startLineNumber !== lineNumber) {
			return;
		}

786 787 788 789 790 791 792 793
		if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) {
			return;
		}

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

M
Matt Bierner 已提交
794
		if (this._commentGlyph && this._commentGlyph.getPosition().position!.lineNumber !== lineNumber) {
795 796 797
			return;
		}

798
		if (e.target.element.className.indexOf('comment-thread') >= 0) {
799
			this.toggleExpand(lineNumber);
800 801 802 803
		}
	}

	private _applyTheme(theme: ITheme) {
M
Matt Bierner 已提交
804
		const borderColor = theme.getColor(peekViewBorder) || Color.transparent;
805 806 807 808
		this.style({
			arrowColor: borderColor,
			frameColor: borderColor
		});
809 810 811 812

		const content: string[] = [];
		const linkColor = theme.getColor(textLinkForeground);
		if (linkColor) {
813
			content.push(`.monaco-editor .review-widget .body .comment-body a { color: ${linkColor} }`);
814 815 816 817
		}

		const linkActiveColor = theme.getColor(textLinkActiveForeground);
		if (linkActiveColor) {
818
			content.push(`.monaco-editor .review-widget .body .comment-body a:hover, a:active { color: ${linkActiveColor} }`);
819 820 821 822
		}

		const focusColor = theme.getColor(focusBorder);
		if (focusColor) {
823 824
			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}; }`);
825 826
		}

827 828 829 830 831 832 833 834 835 836
		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}; }`);
		}

837 838 839
		const hcBorder = theme.getColor(contrastBorder);
		if (hcBorder) {
			content.push(`.monaco-editor .review-widget .body .comment-form .review-thread-reply-button { outline-color: ${hcBorder}; }`);
840
			content.push(`.monaco-editor .review-widget .body .monaco-editor { outline: 1px solid ${hcBorder}; }`);
841 842
		}

843 844
		const errorBorder = theme.getColor(inputValidationErrorBorder);
		if (errorBorder) {
845
			content.push(`.monaco-editor .review-widget .validation-error { border: 1px solid ${errorBorder}; }`);
846 847 848 849
		}

		const errorBackground = theme.getColor(inputValidationErrorBackground);
		if (errorBackground) {
850
			content.push(`.monaco-editor .review-widget .validation-error { background: ${errorBackground}; }`);
851 852 853 854 855
		}

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

858 859 860 861 862 863 864
		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};
		}`);

865
		this._styleElement.innerHTML = content.join('\n');
866 867 868

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

871
	show(rangeOrPos: IRange | IPosition, heightInLines: number): void {
872
		this._isExpanded = true;
873
		super.show(rangeOrPos, heightInLines);
874
		this._refresh();
875 876 877
	}

	hide() {
878
		this._isExpanded = false;
879 880
		// Focus the container so that the comment editor will be blurred before it is hidden
		this.editor.focus();
881 882 883
		super.hide();
	}

884 885 886 887 888 889
	dispose() {
		super.dispose();
		if (this._resizeObserver) {
			this._resizeObserver.disconnect();
			this._resizeObserver = null;
		}
890

891
		if (this._commentGlyph) {
892
			this._commentGlyph.dispose();
893
			this._commentGlyph = undefined;
894
		}
895

896
		this._globalToDispose.dispose();
P
Peng Lyu 已提交
897
		this._commentThreadDisposables.forEach(global => global.dispose());
898
		this._submitActionsDisposables.forEach(local => local.dispose());
899
		this._onDidClose.fire(undefined);
900 901
	}
}