<commit_before>/*---------------------------------------------------------------------------------------------
 *  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, dispose } from 'vs/base/common/lifecycle';
import * as strings from 'vs/base/common/strings';
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 { ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget';
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService';
import { CommentGlyphWidget } from 'vs/workbench/contrib/comments/electron-browser/commentGlyphWidget';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IModelService } from 'vs/editor/common/services/modelService';
import { SimpleCommentEditor } from './simpleCommentEditor';
import { URI } from 'vs/base/common/uri';
import { transparent, editorForeground, textLinkActiveForeground, textLinkForeground, focusBorder, textBlockQuoteBackground, textBlockQuoteBorder, contrastBorder, inputValidationErrorBorder, inputValidationErrorBackground, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry';
import { IModeService } from 'vs/editor/common/services/modeService';
import { ICommentService } from 'vs/workbench/contrib/comments/electron-browser/commentService';
import { Range, IRange } from 'vs/editor/common/core/range';
import { IPosition } from 'vs/editor/common/core/position';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer';
import { IMarginData } from 'vs/editor/browser/controller/mouseTarget';
import { CommentNode } from 'vs/workbench/contrib/comments/electron-browser/commentNode';
import { ITextModel } from 'vs/editor/common/model';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { generateUuid } from 'vs/base/common/uuid';
import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget';

export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration';
const COLLAPSE_ACTION_CLASS = 'expand-review-action octicon octicon-x';
const COMMENT_SCHEME = 'comment';


let INMEM_MODEL_ID = 0;

export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget {
	private _headElement: HTMLElement;
	protected _headingLabel: HTMLElement;
	protected _actionbarWidget: ActionBar;
	private _bodyElement: HTMLElement;
	private _parentEditor: ICodeEditor;
	private _commentEditor: ICodeEditor;
	private _commentsElement: HTMLElement;
	private _commentElements: CommentNode[];
	private _commentForm: HTMLElement;
	private _reviewThreadReplyButton: HTMLElement;
	private _resizeObserver: any;
	private _onDidClose = new Emitter<ReviewZoneWidget>();
	private _onDidCreateThread = new Emitter<ReviewZoneWidget>();
	private _isCollapsed;
	private _collapseAction: Action;
	private _commentGlyph: CommentGlyphWidget;
	private _submitActionsDisposables: IDisposable[];
	private _globalToDispose: IDisposable[];
	private _markdownRenderer: MarkdownRenderer;
	private _styleElement: HTMLStyleElement;
	private _formActions: HTMLElement;
	private _error: HTMLElement;

	public get owner(): string {
		return this._owner;
	}
	public get commentThread(): modes.CommentThread {
		return this._commentThread;
	}

	public get extensionId(): string {
		return this._commentThread.extensionId;
	}

	public get draftMode(): modes.DraftMode {
		return this._draftMode;
	}

	constructor(
		editor: ICodeEditor,
		private _owner: string,
		private _commentThread: modes.CommentThread | modes.CommentThread2,
		private _pendingComment: string,
		private _draftMode: modes.DraftMode,
		@IInstantiationService private instantiationService: IInstantiationService,
		@IModeService private modeService: IModeService,
		@ICommandService private commandService: ICommandService,
		@IModelService private modelService: IModelService,
		@IThemeService private themeService: IThemeService,
		@ICommentService private commentService: ICommentService,
		@IOpenerService private openerService: IOpenerService
	) {
		super(editor, { keepEditorSelection: true });
		this._resizeObserver = null;
		this._isCollapsed = _commentThread.collapsibleState !== modes.CommentThreadCollapsibleState.Expanded;
		this._globalToDispose = [];
		this._submitActionsDisposables = [];
		this._formActions = null;
		this.create();

		this._styleElement = dom.createStyleSheet(this.domNode);
		this._globalToDispose.push(this.themeService.onThemeChange(this._applyTheme, this));
		this._globalToDispose.push(this.editor.onDidChangeConfiguration(e => {
			if (e.fontInfo) {
				this._applyTheme(this.themeService.getTheme());
			}
		}));
		this._applyTheme(this.themeService.getTheme());

		this._markdownRenderer = new MarkdownRenderer(editor, this.modeService, this.openerService);
		this._parentEditor = editor;
	}

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

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

	public getPosition(): IPosition | undefined {
		let position: IPosition = this.position;
		if (position) {
			return position;
		}

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

	protected revealLine(lineNumber: number) {
		// we don't do anything here as we always do the reveal ourselves.
	}

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

		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;
			}
		}

		this.editor.revealRangeInCenter(this._commentThread.range);
	}

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

			if (model && model.getValueLength() > 0) { // checking length is cheap
				return model.getValue();
			}
		}

		return null;
	}

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

		this._bodyElement = <HTMLDivElement>dom.$('.body');
		container.appendChild(this._bodyElement);

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

	protected _fillHead(container: HTMLElement): void {
		let titleElement = dom.append(this._headElement, dom.$('.review-title'));

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

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

		this._collapseAction = new Action('review.expand', nls.localize('label.collapse', "Collapse"), COLLAPSE_ACTION_CLASS, true, () => {
			if (this._commentThread.comments.length === 0 && (this._commentThread as modes.CommentThread2).commentThreadHandle === undefined) {
				this.dispose();
				return null;
			}

			this._isCollapsed = true;
			this.hide();
			return null;
		});

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

	public getGlyphPosition(): number {
		return this._commentGlyph.getPosition().position.lineNumber;
	}

	toggleExpand(lineNumber: number) {
		if (this._isCollapsed) {
			this.show({ lineNumber: lineNumber, column: 1 }, 2);
		} else {
			this.hide();
			if (this._commentThread === null || this._commentThread.threadId === null) {
				this.dispose();
			}
		}
	}

	update(commentThread: modes.CommentThread | modes.CommentThread2) {
		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--) {
			this._commentElements.splice(commentElementsToDelIndex[i], 1);
			this._commentsElement.removeChild(commentElementsToDel[i].domNode);
		}

		let lastCommentElement: HTMLElement | null = null;
		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) {
				oldCommentNode[0].update(currentComment);
				lastCommentElement = oldCommentNode[0].domNode;
				newCommentNodeList.unshift(oldCommentNode[0]);
			} else {
				const newElement = this.createNewCommentNode(currentComment);

				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;
		this.createThreadLabel();

		// 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);
		}

		if (!this._reviewThreadReplyButton) {
			this.createReplyButton();
		}

		if (!this._isCollapsed) {
			this.show({ lineNumber, column: 1 }, 2);
		}
	}

	updateDraftMode(draftMode: modes.DraftMode) {
		if (this._draftMode !== draftMode) {
			this._draftMode = draftMode;

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

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

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

	display(lineNumber: number) {
		this._commentGlyph = new CommentGlyphWidget(this.editor, lineNumber);

		this._disposables.push(this.editor.onMouseDown(e => this.onEditorMouseDown(e)));
		this._disposables.push(this.editor.onMouseUp(e => this.onEditorMouseUp(e)));

		let headHeight = Math.ceil(this.editor.getConfiguration().lineHeight * 1.2);
		this._headElement.style.height = `${headHeight}px`;
		this._headElement.style.lineHeight = this._headElement.style.height;

		this._commentsElement = dom.append(this._bodyElement, dom.$('div.comments-container'));
		this._commentsElement.setAttribute('role', 'presentation');

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

			this._commentElements.push(newCommentNode);
			this._commentsElement.appendChild(newCommentNode.domNode);
		}

		const hasExistingComments = this._commentThread.comments.length > 0;
		this._commentForm = dom.append(this._bodyElement, dom.$('.comment-form'));
		this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, this._commentForm, SimpleCommentEditor.getEditorOptions(), this._parentEditor, this);

		const modeId = generateUuid() + '-' + (hasExistingComments ? this._commentThread.threadId : ++INMEM_MODEL_ID);
		const params = JSON.stringify({
			extensionId: this.extensionId,
			commentThreadId: this.commentThread.threadId
		});
		const resource = URI.parse(`${COMMENT_SCHEME}:commentinput-${modeId}.md?${params}`);
		const model = this.modelService.createModel(this._pendingComment || '', this.modeService.createByFilepathOrFirstLine(resource.path), resource, false);
		this._disposables.push(model);
		this._commentEditor.setModel(model);
		this._disposables.push(this._commentEditor);
		this._disposables.push(this._commentEditor.getModel().onDidChangeContent(() => this.setCommentEditorDecorations()));
		if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) {
			this._disposables.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);
			}));

			this._disposables.push(this._commentEditor.getModel().onDidChangeContent(() => {
				let modelContent = this._commentEditor.getValue();
				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;
				}
			}));

			this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeInput(input => {
				let thread = (this._commentThread as modes.CommentThread2);

				if (thread.input.uri !== this._commentEditor.getModel().uri) {
					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');
					}
				}
			}));

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

		this.setCommentEditorDecorations();

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

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

		this._formActions = dom.append(this._commentForm, dom.$('.form-actions'));
		if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) {
			this.createCommentWidgetActions2(this._formActions, model);

			this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeAcceptInputCommands(_ => {
				dom.clearNode(this._formActions);
				this.createCommentWidgetActions2(this._formActions, model);<commit_msg>Update Comment Thread label.<commit_after>/*---------------------------------------------------------------------------------------------
 *  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, dispose } from 'vs/base/common/lifecycle';
import * as strings from 'vs/base/common/strings';
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 { ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget';
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService';
import { CommentGlyphWidget } from 'vs/workbench/contrib/comments/electron-browser/commentGlyphWidget';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IModelService } from 'vs/editor/common/services/modelService';
import { SimpleCommentEditor } from './simpleCommentEditor';
import { URI } from 'vs/base/common/uri';
import { transparent, editorForeground, textLinkActiveForeground, textLinkForeground, focusBorder, textBlockQuoteBackground, textBlockQuoteBorder, contrastBorder, inputValidationErrorBorder, inputValidationErrorBackground, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry';
import { IModeService } from 'vs/editor/common/services/modeService';
import { ICommentService } from 'vs/workbench/contrib/comments/electron-browser/commentService';
import { Range, IRange } from 'vs/editor/common/core/range';
import { IPosition } from 'vs/editor/common/core/position';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer';
import { IMarginData } from 'vs/editor/browser/controller/mouseTarget';
import { CommentNode } from 'vs/workbench/contrib/comments/electron-browser/commentNode';
import { ITextModel } from 'vs/editor/common/model';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { generateUuid } from 'vs/base/common/uuid';
import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget';

export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration';
const COLLAPSE_ACTION_CLASS = 'expand-review-action octicon octicon-x';
const COMMENT_SCHEME = 'comment';


let INMEM_MODEL_ID = 0;

export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget {
	private _headElement: HTMLElement;
	protected _headingLabel: HTMLElement;
	protected _actionbarWidget: ActionBar;
	private _bodyElement: HTMLElement;
	private _parentEditor: ICodeEditor;
	private _commentEditor: ICodeEditor;
	private _commentsElement: HTMLElement;
	private _commentElements: CommentNode[];
	private _commentForm: HTMLElement;
	private _reviewThreadReplyButton: HTMLElement;
	private _resizeObserver: any;
	private _onDidClose = new Emitter<ReviewZoneWidget>();
	private _onDidCreateThread = new Emitter<ReviewZoneWidget>();
	private _isCollapsed;
	private _collapseAction: Action;
	private _commentGlyph: CommentGlyphWidget;
	private _submitActionsDisposables: IDisposable[];
	private _globalToDispose: IDisposable[];
	private _markdownRenderer: MarkdownRenderer;
	private _styleElement: HTMLStyleElement;
	private _formActions: HTMLElement;
	private _error: HTMLElement;

	public get owner(): string {
		return this._owner;
	}
	public get commentThread(): modes.CommentThread {
		return this._commentThread;
	}

	public get extensionId(): string {
		return this._commentThread.extensionId;
	}

	public get draftMode(): modes.DraftMode {
		return this._draftMode;
	}

	constructor(
		editor: ICodeEditor,
		private _owner: string,
		private _commentThread: modes.CommentThread | modes.CommentThread2,
		private _pendingComment: string,
		private _draftMode: modes.DraftMode,
		@IInstantiationService private instantiationService: IInstantiationService,
		@IModeService private modeService: IModeService,
		@ICommandService private commandService: ICommandService,
		@IModelService private modelService: IModelService,
		@IThemeService private themeService: IThemeService,
		@ICommentService private commentService: ICommentService,
		@IOpenerService private openerService: IOpenerService
	) {
		super(editor, { keepEditorSelection: true });
		this._resizeObserver = null;
		this._isCollapsed = _commentThread.collapsibleState !== modes.CommentThreadCollapsibleState.Expanded;
		this._globalToDispose = [];
		this._submitActionsDisposables = [];
		this._formActions = null;
		this.create();

		this._styleElement = dom.createStyleSheet(this.domNode);
		this._globalToDispose.push(this.themeService.onThemeChange(this._applyTheme, this));
		this._globalToDispose.push(this.editor.onDidChangeConfiguration(e => {
			if (e.fontInfo) {
				this._applyTheme(this.themeService.getTheme());
			}
		}));
		this._applyTheme(this.themeService.getTheme());

		this._markdownRenderer = new MarkdownRenderer(editor, this.modeService, this.openerService);
		this._parentEditor = editor;
	}

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

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

	public getPosition(): IPosition | undefined {
		let position: IPosition = this.position;
		if (position) {
			return position;
		}

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

	protected revealLine(lineNumber: number) {
		// we don't do anything here as we always do the reveal ourselves.
	}

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

		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;
			}
		}

		this.editor.revealRangeInCenter(this._commentThread.range);
	}

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

			if (model && model.getValueLength() > 0) { // checking length is cheap
				return model.getValue();
			}
		}

		return null;
	}

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

		this._bodyElement = <HTMLDivElement>dom.$('.body');
		container.appendChild(this._bodyElement);

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

	protected _fillHead(container: HTMLElement): void {
		let titleElement = dom.append(this._headElement, dom.$('.review-title'));

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

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

		this._collapseAction = new Action('review.expand', nls.localize('label.collapse', "Collapse"), COLLAPSE_ACTION_CLASS, true, () => {
			if (this._commentThread.comments.length === 0 && (this._commentThread as modes.CommentThread2).commentThreadHandle === undefined) {
				this.dispose();
				return null;
			}

			this._isCollapsed = true;
			this.hide();
			return null;
		});

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

	public getGlyphPosition(): number {
		return this._commentGlyph.getPosition().position.lineNumber;
	}

	toggleExpand(lineNumber: number) {
		if (this._isCollapsed) {
			this.show({ lineNumber: lineNumber, column: 1 }, 2);
		} else {
			this.hide();
			if (this._commentThread === null || this._commentThread.threadId === null) {
				this.dispose();
			}
		}
	}

	update(commentThread: modes.CommentThread | modes.CommentThread2) {
		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--) {
			this._commentElements.splice(commentElementsToDelIndex[i], 1);
			this._commentsElement.removeChild(commentElementsToDel[i].domNode);
		}

		let lastCommentElement: HTMLElement | null = null;
		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) {
				oldCommentNode[0].update(currentComment);
				lastCommentElement = oldCommentNode[0].domNode;
				newCommentNodeList.unshift(oldCommentNode[0]);
			} else {
				const newElement = this.createNewCommentNode(currentComment);

				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;
		this.createThreadLabel();

		// 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);
		}

		if (!this._reviewThreadReplyButton) {
			this.createReplyButton();
		}

		if (!this._isCollapsed) {
			this.show({ lineNumber, column: 1 }, 2);
		}
	}

	updateDraftMode(draftMode: modes.DraftMode) {
		if (this._draftMode !== draftMode) {
			this._draftMode = draftMode;

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

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

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

	display(lineNumber: number) {
		this._commentGlyph = new CommentGlyphWidget(this.editor, lineNumber);

		this._disposables.push(this.editor.onMouseDown(e => this.onEditorMouseDown(e)));
		this._disposables.push(this.editor.onMouseUp(e => this.onEditorMouseUp(e)));

		let headHeight = Math.ceil(this.editor.getConfiguration().lineHeight * 1.2);
		this._headElement.style.height = `${headHeight}px`;
		this._headElement.style.lineHeight = this._headElement.style.height;

		this._commentsElement = dom.append(this._bodyElement, dom.$('div.comments-container'));
		this._commentsElement.setAttribute('role', 'presentation');

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

			this._commentElements.push(newCommentNode);
			this._commentsElement.appendChild(newCommentNode.domNode);
		}

		const hasExistingComments = this._commentThread.comments.length > 0;
		this._commentForm = dom.append(this._bodyElement, dom.$('.comment-form'));
		this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, this._commentForm, SimpleCommentEditor.getEditorOptions(), this._parentEditor, this);

		const modeId = generateUuid() + '-' + (hasExistingComments ? this._commentThread.threadId : ++INMEM_MODEL_ID);
		const params = JSON.stringify({
			extensionId: this.extensionId,
			commentThreadId: this.commentThread.threadId
		});
		const resource = URI.parse(`${COMMENT_SCHEME}:commentinput-${modeId}.md?${params}`);
		const model = this.modelService.createModel(this._pendingComment || '', this.modeService.createByFilepathOrFirstLine(resource.path), resource, false);
		this._disposables.push(model);
		this._commentEditor.setModel(model);
		this._disposables.push(this._commentEditor);
		this._disposables.push(this._commentEditor.getModel().onDidChangeContent(() => this.setCommentEditorDecorations()));
		if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) {
			this._disposables.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);
			}));

			this._disposables.push(this._commentEditor.getModel().onDidChangeContent(() => {
				let modelContent = this._commentEditor.getValue();
				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;
				}
			}));

			this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeInput(input => {
				let thread = (this._commentThread as modes.CommentThread2);

				if (thread.input.uri !== this._commentEditor.getModel().uri) {
					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');
					}
				}
			}));

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

			this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeLabel(_ => {
				this.createThreadLabel();
			}));
		}

		this.setCommentEditorDecorations();

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

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

		this._formActions = dom.append(this._commentForm, dom.$('.form-actions'));
		if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) {
			this.createCommentWidgetActions2(this._formActions, model);

			this._disposables.push((this._commentThread as modes.CommentThread2).onDidChangeAcceptInputCommands(_ => {
				dom.clearNode(this._formActions);
				this.createCommentWidgetActions2(this._formActions, model);
{'fraction_non_alphanumeric': 0.13236867126218207, 'fraction_numerical': 0.0037140480152127405, 'mean_word_length': 4.7503844182470525, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 52, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7152637', 'n_tokens_mistral': 10888, 'n_tokens_neox': 9737, 'n_words': 1860}