Path: blob/main/src/vs/workbench/contrib/comments/browser/commentsController.ts
5239 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Action, IAction } from '../../../../base/common/actions.js';6import { coalesce } from '../../../../base/common/arrays.js';7import { findFirstIdxMonotonousOrArrLen } from '../../../../base/common/arraysFind.js';8import { CancelablePromise, createCancelablePromise, Delayer } from '../../../../base/common/async.js';9import { onUnexpectedError } from '../../../../base/common/errors.js';10import { Disposable, DisposableStore, dispose, IDisposable } from '../../../../base/common/lifecycle.js';11import './media/review.css';12import { ICodeEditor, IEditorMouseEvent, isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js';13import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';14import { IRange, Range } from '../../../../editor/common/core/range.js';15import { EditorType, IDiffEditor, IEditor, IEditorContribution, IModelChangedEvent } from '../../../../editor/common/editorCommon.js';16import { IModelDecorationOptions, IModelDeltaDecoration } from '../../../../editor/common/model.js';17import { ModelDecorationOptions, TextModel } from '../../../../editor/common/model/textModel.js';18import * as languages from '../../../../editor/common/languages.js';19import * as nls from '../../../../nls.js';20import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';21import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';22import { IQuickInputService, IQuickPickItem, QuickPickInput } from '../../../../platform/quickinput/common/quickInput.js';23import { CommentGlyphWidget } from './commentGlyphWidget.js';24import { ICommentInfo, ICommentService } from './commentService.js';25import { CommentWidgetFocus, isMouseUpEventDragFromMouseDown, parseMouseDownInfoFromEvent, ReviewZoneWidget } from './commentThreadZoneWidget.js';26import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js';27import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js';28import { EditorOption } from '../../../../editor/common/config/editorOptions.js';29import { IViewsService } from '../../../services/views/common/viewsService.js';30import { COMMENTS_VIEW_ID } from './commentsTreeViewer.js';31import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';32import { COMMENTS_SECTION, ICommentsConfiguration } from '../common/commentsConfiguration.js';33import { COMMENTEDITOR_DECORATION_KEY } from './commentReply.js';34import { Emitter } from '../../../../base/common/event.js';35import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';36import { Position } from '../../../../editor/common/core/position.js';37import { CommentThreadRangeDecorator } from './commentThreadRangeDecorator.js';38import { ICursorSelectionChangedEvent } from '../../../../editor/common/cursorEvents.js';39import { CommentsPanel } from './commentsView.js';40import { status } from '../../../../base/browser/ui/aria/aria.js';41import { CommentContextKeys } from '../common/commentContextKeys.js';42import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js';43import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js';44import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';45import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';46import { URI } from '../../../../base/common/uri.js';47import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';48import { threadHasMeaningfulComments } from './commentsModel.js';49import { INotificationService } from '../../../../platform/notification/common/notification.js';5051export const ID = 'editor.contrib.review';5253interface CommentRangeAction {54ownerId: string;55extensionId: string | undefined;56label: string | undefined;57commentingRangesInfo: languages.CommentingRanges;58}5960interface MergedCommentRangeActions {61range?: Range;62action: CommentRangeAction;63}6465class CommentingRangeDecoration implements IModelDeltaDecoration {66private _decorationId: string | undefined;67private _startLineNumber: number;68private _endLineNumber: number;6970public get id(): string | undefined {71return this._decorationId;72}7374public set id(id: string | undefined) {75this._decorationId = id;76}7778public get range(): IRange {79return {80startLineNumber: this._startLineNumber, startColumn: 1,81endLineNumber: this._endLineNumber, endColumn: 182};83}8485constructor(private _editor: ICodeEditor, private _ownerId: string, private _extensionId: string | undefined, private _label: string | undefined, private _range: IRange, public readonly options: ModelDecorationOptions, private commentingRangesInfo: languages.CommentingRanges, public readonly isHover: boolean = false) {86this._startLineNumber = _range.startLineNumber;87this._endLineNumber = _range.endLineNumber;88}8990public getCommentAction(): CommentRangeAction {91return {92extensionId: this._extensionId,93label: this._label,94ownerId: this._ownerId,95commentingRangesInfo: this.commentingRangesInfo96};97}9899public getOriginalRange() {100return this._range;101}102103public getActiveRange() {104return this.id ? this._editor.getModel()!.getDecorationRange(this.id) : undefined;105}106}107108class CommentingRangeDecorator {109public static description = 'commenting-range-decorator';110private decorationOptions: ModelDecorationOptions;111private hoverDecorationOptions: ModelDecorationOptions;112private multilineDecorationOptions: ModelDecorationOptions;113private commentingRangeDecorations: CommentingRangeDecoration[] = [];114private decorationIds: string[] = [];115private _editor: ICodeEditor | undefined;116private _infos: ICommentInfo[] | undefined;117private _lastHover: number = -1;118private _lastSelection: Range | undefined;119private _lastSelectionCursor: number | undefined;120private _onDidChangeDecorationsCount: Emitter<number> = new Emitter();121public readonly onDidChangeDecorationsCount = this._onDidChangeDecorationsCount.event;122123constructor() {124const decorationOptions: IModelDecorationOptions = {125description: CommentingRangeDecorator.description,126isWholeLine: true,127linesDecorationsClassName: 'comment-range-glyph comment-diff-added'128};129130this.decorationOptions = ModelDecorationOptions.createDynamic(decorationOptions);131132const hoverDecorationOptions: IModelDecorationOptions = {133description: CommentingRangeDecorator.description,134isWholeLine: true,135linesDecorationsClassName: `comment-range-glyph line-hover`136};137138this.hoverDecorationOptions = ModelDecorationOptions.createDynamic(hoverDecorationOptions);139140const multilineDecorationOptions: IModelDecorationOptions = {141description: CommentingRangeDecorator.description,142isWholeLine: true,143linesDecorationsClassName: `comment-range-glyph multiline-add`144};145146this.multilineDecorationOptions = ModelDecorationOptions.createDynamic(multilineDecorationOptions);147}148149public updateHover(hoverLine?: number) {150if (this._editor && this._infos && (hoverLine !== this._lastHover)) {151this._doUpdate(this._editor, this._infos, hoverLine);152}153this._lastHover = hoverLine ?? -1;154}155156public updateSelection(cursorLine: number, range: Range = new Range(0, 0, 0, 0)) {157this._lastSelection = range.isEmpty() ? undefined : range;158this._lastSelectionCursor = range.isEmpty() ? undefined : cursorLine;159// Some scenarios:160// Selection is made. Emphasis should show on the drag/selection end location.161// Selection is made, then user clicks elsewhere. We should still show the decoration.162if (this._editor && this._infos) {163this._doUpdate(this._editor, this._infos, cursorLine, range);164}165}166167public update(editor: ICodeEditor | undefined, commentInfos: ICommentInfo[], cursorLine?: number, range?: Range) {168if (editor) {169this._editor = editor;170this._infos = commentInfos;171this._doUpdate(editor, commentInfos, cursorLine, range);172}173}174175private _lineHasThread(editor: ICodeEditor, lineRange: Range) {176return editor.getDecorationsInRange(lineRange)?.find(decoration => decoration.options.description === CommentGlyphWidget.description);177}178179private _doUpdate(editor: ICodeEditor, commentInfos: ICommentInfo[], emphasisLine: number = -1, selectionRange: Range | undefined = this._lastSelection) {180const model = editor.getModel();181if (!model) {182return;183}184185// If there's still a selection, use that.186emphasisLine = this._lastSelectionCursor ?? emphasisLine;187188const commentingRangeDecorations: CommentingRangeDecoration[] = [];189for (const info of commentInfos) {190info.commentingRanges.ranges.forEach(range => {191const rangeObject = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn);192let intersectingSelectionRange = selectionRange ? rangeObject.intersectRanges(selectionRange) : undefined;193if ((selectionRange && (emphasisLine >= 0) && intersectingSelectionRange)194// If there's only one selection line, then just drop into the else if and show an emphasis line.195&& !((intersectingSelectionRange.startLineNumber === intersectingSelectionRange.endLineNumber)196&& (emphasisLine === intersectingSelectionRange.startLineNumber))) {197// The emphasisLine should be within the commenting range, even if the selection range stretches198// outside of the commenting range.199// Clip the emphasis and selection ranges to the commenting range200let intersectingEmphasisRange: Range;201if (emphasisLine <= intersectingSelectionRange.startLineNumber) {202intersectingEmphasisRange = intersectingSelectionRange.collapseToStart();203intersectingSelectionRange = new Range(intersectingSelectionRange.startLineNumber + 1, 1, intersectingSelectionRange.endLineNumber, 1);204} else {205intersectingEmphasisRange = new Range(intersectingSelectionRange.endLineNumber, 1, intersectingSelectionRange.endLineNumber, 1);206intersectingSelectionRange = new Range(intersectingSelectionRange.startLineNumber, 1, intersectingSelectionRange.endLineNumber - 1, 1);207}208commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, intersectingSelectionRange, this.multilineDecorationOptions, info.commentingRanges, true));209210if (!this._lineHasThread(editor, intersectingEmphasisRange)) {211commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, intersectingEmphasisRange, this.hoverDecorationOptions, info.commentingRanges, true));212}213214const beforeRangeEndLine = Math.min(intersectingEmphasisRange.startLineNumber, intersectingSelectionRange.startLineNumber) - 1;215const hasBeforeRange = rangeObject.startLineNumber <= beforeRangeEndLine;216const afterRangeStartLine = Math.max(intersectingEmphasisRange.endLineNumber, intersectingSelectionRange.endLineNumber) + 1;217const hasAfterRange = rangeObject.endLineNumber >= afterRangeStartLine;218if (hasBeforeRange) {219const beforeRange = new Range(range.startLineNumber, 1, beforeRangeEndLine, 1);220commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, beforeRange, this.decorationOptions, info.commentingRanges, true));221}222if (hasAfterRange) {223const afterRange = new Range(afterRangeStartLine, 1, range.endLineNumber, 1);224commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, afterRange, this.decorationOptions, info.commentingRanges, true));225}226} else if ((rangeObject.startLineNumber <= emphasisLine) && (emphasisLine <= rangeObject.endLineNumber)) {227if (rangeObject.startLineNumber < emphasisLine) {228const beforeRange = new Range(range.startLineNumber, 1, emphasisLine - 1, 1);229commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, beforeRange, this.decorationOptions, info.commentingRanges, true));230}231const emphasisRange = new Range(emphasisLine, 1, emphasisLine, 1);232if (!this._lineHasThread(editor, emphasisRange)) {233commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, emphasisRange, this.hoverDecorationOptions, info.commentingRanges, true));234}235if (emphasisLine < rangeObject.endLineNumber) {236const afterRange = new Range(emphasisLine + 1, 1, range.endLineNumber, 1);237commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, afterRange, this.decorationOptions, info.commentingRanges, true));238}239} else {240commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.uniqueOwner, info.extensionId, info.label, range, this.decorationOptions, info.commentingRanges));241}242});243}244245editor.changeDecorations((accessor) => {246this.decorationIds = accessor.deltaDecorations(this.decorationIds, commentingRangeDecorations);247commentingRangeDecorations.forEach((decoration, index) => decoration.id = this.decorationIds[index]);248});249250const rangesDifference = this.commentingRangeDecorations.length - commentingRangeDecorations.length;251this.commentingRangeDecorations = commentingRangeDecorations;252if (rangesDifference) {253this._onDidChangeDecorationsCount.fire(this.commentingRangeDecorations.length);254}255}256257private areRangesIntersectingOrTouchingByLine(a: Range, b: Range) {258// Check if `a` is before `b`259if (a.endLineNumber < (b.startLineNumber - 1)) {260return false;261}262263// Check if `b` is before `a`264if ((b.endLineNumber + 1) < a.startLineNumber) {265return false;266}267268// These ranges must intersect269return true;270}271272public getMatchedCommentAction(commentRange: Range | undefined): MergedCommentRangeActions[] {273if (commentRange === undefined) {274const foundInfos = this._infos?.filter(info => info.commentingRanges.fileComments);275if (foundInfos) {276return foundInfos.map(foundInfo => {277return {278action: {279ownerId: foundInfo.uniqueOwner,280extensionId: foundInfo.extensionId,281label: foundInfo.label,282commentingRangesInfo: foundInfo.commentingRanges283}284};285});286}287return [];288}289290// keys is ownerId291const foundHoverActions = new Map<string, { range: Range; action: CommentRangeAction }>();292for (const decoration of this.commentingRangeDecorations) {293const range = decoration.getActiveRange();294if (range && this.areRangesIntersectingOrTouchingByLine(range, commentRange)) {295// We can have several commenting ranges that match from the same uniqueOwner because of how296// the line hover and selection decoration is done.297// The ranges must be merged so that we can see if the new commentRange fits within them.298const action = decoration.getCommentAction();299const alreadyFoundInfo = foundHoverActions.get(action.ownerId);300if (alreadyFoundInfo?.action.commentingRangesInfo === action.commentingRangesInfo) {301// Merge ranges.302const newRange = new Range(303range.startLineNumber < alreadyFoundInfo.range.startLineNumber ? range.startLineNumber : alreadyFoundInfo.range.startLineNumber,304range.startColumn < alreadyFoundInfo.range.startColumn ? range.startColumn : alreadyFoundInfo.range.startColumn,305range.endLineNumber > alreadyFoundInfo.range.endLineNumber ? range.endLineNumber : alreadyFoundInfo.range.endLineNumber,306range.endColumn > alreadyFoundInfo.range.endColumn ? range.endColumn : alreadyFoundInfo.range.endColumn307);308foundHoverActions.set(action.ownerId, { range: newRange, action });309} else {310foundHoverActions.set(action.ownerId, { range, action });311}312}313}314315const seenOwners = new Set<string>();316return Array.from(foundHoverActions.values()).filter(action => {317if (seenOwners.has(action.action.ownerId)) {318return false;319} else {320seenOwners.add(action.action.ownerId);321return true;322}323});324}325326public getNearestCommentingRange(findPosition: Position, reverse?: boolean): Range | undefined {327let findPositionContainedWithin: Range | undefined;328let decorations: CommentingRangeDecoration[];329if (reverse) {330decorations = [];331for (let i = this.commentingRangeDecorations.length - 1; i >= 0; i--) {332decorations.push(this.commentingRangeDecorations[i]);333}334} else {335decorations = this.commentingRangeDecorations;336}337for (const decoration of decorations) {338const range = decoration.getActiveRange();339if (!range) {340continue;341}342343if (findPositionContainedWithin && this.areRangesIntersectingOrTouchingByLine(range, findPositionContainedWithin)) {344findPositionContainedWithin = Range.plusRange(findPositionContainedWithin, range);345continue;346}347348if (range.startLineNumber <= findPosition.lineNumber && findPosition.lineNumber <= range.endLineNumber) {349findPositionContainedWithin = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn);350continue;351}352353if (!reverse && range.endLineNumber < findPosition.lineNumber) {354continue;355}356357if (reverse && range.startLineNumber > findPosition.lineNumber) {358continue;359}360361return range;362}363return (decorations.length > 0 ? (decorations[0].getActiveRange() ?? undefined) : undefined);364}365366public dispose(): void {367this._onDidChangeDecorationsCount.dispose();368this.commentingRangeDecorations = [];369}370}371372/**373* Navigate to the next or previous comment in the current thread.374* @param type375*/376export function moveToNextCommentInThread(commentInfo: { thread: languages.CommentThread<IRange>; comment?: languages.Comment } | undefined, type: 'next' | 'previous') {377if (!commentInfo?.comment || !commentInfo?.thread?.comments) {378return;379}380const currentIndex = commentInfo.thread.comments?.indexOf(commentInfo.comment);381if (currentIndex === undefined || currentIndex < 0) {382return;383}384if (type === 'previous' && currentIndex === 0) {385return;386}387if (type === 'next' && currentIndex === commentInfo.thread.comments.length - 1) {388return;389}390const comment = commentInfo.thread.comments?.[type === 'previous' ? currentIndex - 1 : currentIndex + 1];391if (!comment) {392return;393}394return {395...commentInfo,396comment,397};398}399400export function revealCommentThread(commentService: ICommentService, editorService: IEditorService, uriIdentityService: IUriIdentityService,401commentThread: languages.CommentThread<IRange>, comment: languages.Comment | undefined, focusReply?: boolean, pinned?: boolean, preserveFocus?: boolean, sideBySide?: boolean): void {402if (!commentThread.resource) {403return;404}405if (!commentService.isCommentingEnabled) {406commentService.enableCommenting(true);407}408409const range = commentThread.range;410const focus = focusReply ? CommentWidgetFocus.Editor : (preserveFocus ? CommentWidgetFocus.None : CommentWidgetFocus.Widget);411412const activeEditor = editorService.activeTextEditorControl;413// If the active editor is a diff editor where one of the sides has the comment,414// then we try to reveal the comment in the diff editor.415const currentActiveResources: IEditor[] = isDiffEditor(activeEditor) ? [activeEditor.getOriginalEditor(), activeEditor.getModifiedEditor()]416: (activeEditor ? [activeEditor] : []);417const threadToReveal = commentThread.threadId;418const commentToReveal = comment?.uniqueIdInThread;419const resource = URI.parse(commentThread.resource);420421for (const editor of currentActiveResources) {422const model = editor.getModel();423if ((model instanceof TextModel) && uriIdentityService.extUri.isEqual(resource, model.uri)) {424425if (threadToReveal && isCodeEditor(editor)) {426const controller = CommentController.get(editor);427controller?.revealCommentThread(threadToReveal, commentToReveal, true, focus);428}429return;430}431}432433editorService.openEditor({434resource,435options: {436pinned: pinned,437preserveFocus: preserveFocus,438selection: range ?? new Range(1, 1, 1, 1)439}440}, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => {441if (editor) {442const control = editor.getControl();443if (threadToReveal && isCodeEditor(control)) {444const controller = CommentController.get(control);445controller?.revealCommentThread(threadToReveal, commentToReveal, true, focus);446}447}448});449}450451export class CommentController extends Disposable implements IEditorContribution {452private readonly localToDispose: DisposableStore = this._register(new DisposableStore());453private editor: ICodeEditor | undefined;454private _commentWidgets: ReviewZoneWidget[];455private _commentInfos: ICommentInfo[];456private _commentingRangeDecorator!: CommentingRangeDecorator;457private _commentThreadRangeDecorator!: CommentThreadRangeDecorator;458private mouseDownInfo: { lineNumber: number } | null = null;459private _commentingRangeSpaceReserved = false;460private _commentingRangeAmountReserved = 0;461private _computePromise: CancelablePromise<Array<ICommentInfo | null>> | null;462private _computeAndSetPromise: Promise<void> | undefined;463private _addInProgress!: boolean;464private _emptyThreadsToAddQueue: [Range | undefined, IEditorMouseEvent | undefined][] = [];465private _computeCommentingRangeScheduler!: Delayer<Array<ICommentInfo | null>> | null;466private _pendingNewCommentCache: { [key: string]: { [key: string]: languages.PendingComment } };467private _pendingEditsCache: { [key: string]: { [key: string]: { [key: number]: languages.PendingComment } } }; // uniqueOwner -> threadId -> uniqueIdInThread -> pending comment468private _inProcessContinueOnComments: Map<string, languages.PendingCommentThread[]> = new Map();469private _editorDisposables: IDisposable[] = [];470private _activeCursorHasCommentingRange: IContextKey<boolean>;471private _activeCursorHasComment: IContextKey<boolean>;472private _activeEditorHasCommentingRange: IContextKey<boolean>;473private _commentWidgetVisible: IContextKey<boolean>;474private _hasRespondedToEditorChange: boolean = false;475476constructor(477editor: ICodeEditor,478@ICommentService private readonly commentService: ICommentService,479@IInstantiationService private readonly instantiationService: IInstantiationService,480@ICodeEditorService private readonly codeEditorService: ICodeEditorService,481@IContextMenuService private readonly contextMenuService: IContextMenuService,482@IQuickInputService private readonly quickInputService: IQuickInputService,483@IViewsService private readonly viewsService: IViewsService,484@IConfigurationService private readonly configurationService: IConfigurationService,485@IContextKeyService contextKeyService: IContextKeyService,486@IEditorService private readonly editorService: IEditorService,487@IKeybindingService private readonly keybindingService: IKeybindingService,488@IAccessibilityService private readonly accessibilityService: IAccessibilityService,489@INotificationService private readonly notificationService: INotificationService490) {491super();492this._commentInfos = [];493this._commentWidgets = [];494this._pendingNewCommentCache = {};495this._pendingEditsCache = {};496this._computePromise = null;497this._activeCursorHasCommentingRange = CommentContextKeys.activeCursorHasCommentingRange.bindTo(contextKeyService);498this._activeCursorHasComment = CommentContextKeys.activeCursorHasComment.bindTo(contextKeyService);499this._activeEditorHasCommentingRange = CommentContextKeys.activeEditorHasCommentingRange.bindTo(contextKeyService);500this._commentWidgetVisible = CommentContextKeys.commentWidgetVisible.bindTo(contextKeyService);501502if (editor instanceof EmbeddedCodeEditorWidget) {503return;504}505506this.editor = editor;507508this._commentingRangeDecorator = new CommentingRangeDecorator();509this._register(this._commentingRangeDecorator.onDidChangeDecorationsCount(count => {510if (count === 0) {511this.clearEditorListeners();512} else if (this._editorDisposables.length === 0) {513this.registerEditorListeners();514}515}));516517this._register(this._commentThreadRangeDecorator = new CommentThreadRangeDecorator(this.commentService));518519this._register(this.commentService.onDidDeleteDataProvider(ownerId => {520if (ownerId) {521delete this._pendingNewCommentCache[ownerId];522delete this._pendingEditsCache[ownerId];523} else {524this._pendingNewCommentCache = {};525this._pendingEditsCache = {};526}527this.beginCompute();528}));529this._register(this.commentService.onDidSetDataProvider(_ => this.beginComputeAndHandleEditorChange()));530this._register(this.commentService.onDidUpdateCommentingRanges(_ => this.beginComputeAndHandleEditorChange()));531532this._register(this.commentService.onDidSetResourceCommentInfos(async e => {533const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri;534if (editorURI && editorURI.toString() === e.resource.toString()) {535await this.setComments(e.commentInfos.filter(commentInfo => commentInfo !== null));536}537}));538539this._register(this.commentService.onDidChangeCommentingEnabled(e => {540if (e) {541this.registerEditorListeners();542this.beginCompute();543} else {544this.tryUpdateReservedSpace();545this.clearEditorListeners();546this._commentingRangeDecorator.update(this.editor, []);547this._commentThreadRangeDecorator.update(this.editor, []);548dispose(this._commentWidgets);549this._commentWidgets = [];550}551}));552553this._register(this.editor.onWillChangeModel(e => this.onWillChangeModel(e)));554this._register(this.editor.onDidChangeModel(_ => this.onModelChanged()));555this._register(this.configurationService.onDidChangeConfiguration(e => {556if (e.affectsConfiguration('diffEditor.renderSideBySide')) {557this.beginCompute();558}559}));560561this.onModelChanged();562this._register(this.codeEditorService.registerDecorationType('comment-controller', COMMENTEDITOR_DECORATION_KEY, {}));563this._register(564this.commentService.registerContinueOnCommentProvider({565provideContinueOnComments: () => {566const pendingComments: languages.PendingCommentThread[] = [];567if (this._commentWidgets) {568for (const zone of this._commentWidgets) {569const zonePendingComments = zone.getPendingComments();570const pendingNewComment = zonePendingComments.newComment;571if (!pendingNewComment) {572continue;573}574let lastCommentBody;575if (zone.commentThread.comments && zone.commentThread.comments.length) {576const lastComment = zone.commentThread.comments[zone.commentThread.comments.length - 1];577if (typeof lastComment.body === 'string') {578lastCommentBody = lastComment.body;579} else {580lastCommentBody = lastComment.body.value;581}582}583584if (pendingNewComment.body !== lastCommentBody) {585pendingComments.push({586uniqueOwner: zone.uniqueOwner,587uri: zone.editor.getModel()!.uri,588range: zone.commentThread.range,589comment: pendingNewComment,590isReply: (zone.commentThread.comments !== undefined) && (zone.commentThread.comments.length > 0)591});592}593}594}595return pendingComments;596}597})598);599600}601602private registerEditorListeners() {603this._editorDisposables = [];604if (!this.editor) {605return;606}607this._editorDisposables.push(this.editor.onMouseMove(e => this.onEditorMouseMove(e)));608this._editorDisposables.push(this.editor.onMouseLeave(() => this.onEditorMouseLeave()));609this._editorDisposables.push(this.editor.onDidChangeCursorPosition(e => this.onEditorChangeCursorPosition(e.position)));610this._editorDisposables.push(this.editor.onDidFocusEditorWidget(() => this.onEditorChangeCursorPosition(this.editor?.getPosition() ?? null)));611this._editorDisposables.push(this.editor.onDidChangeCursorSelection(e => this.onEditorChangeCursorSelection(e)));612this._editorDisposables.push(this.editor.onDidBlurEditorWidget(() => this.onEditorChangeCursorSelection()));613}614615private clearEditorListeners() {616dispose(this._editorDisposables);617this._editorDisposables = [];618}619620private onEditorMouseLeave() {621this._commentingRangeDecorator.updateHover();622}623624private onEditorMouseMove(e: IEditorMouseEvent): void {625const position = e.target.position?.lineNumber;626if (e.event.leftButton.valueOf() && position && this.mouseDownInfo) {627this._commentingRangeDecorator.updateSelection(position, new Range(this.mouseDownInfo.lineNumber, 1, position, 1));628} else {629this._commentingRangeDecorator.updateHover(position);630}631}632633private onEditorChangeCursorSelection(e?: ICursorSelectionChangedEvent): void {634const position = this.editor?.getPosition()?.lineNumber;635if (position) {636this._commentingRangeDecorator.updateSelection(position, e?.selection);637}638}639640private onEditorChangeCursorPosition(e: Position | null) {641if (!e) {642return;643}644const range = Range.fromPositions(e, { column: -1, lineNumber: e.lineNumber });645const decorations = this.editor?.getDecorationsInRange(range);646let hasCommentingRange = false;647if (decorations) {648for (const decoration of decorations) {649if (decoration.options.description === CommentGlyphWidget.description) {650// We don't allow multiple comments on the same line.651hasCommentingRange = false;652break;653} else if (decoration.options.description === CommentingRangeDecorator.description) {654hasCommentingRange = true;655}656}657}658this._activeCursorHasCommentingRange.set(hasCommentingRange);659this._activeCursorHasComment.set(this.getCommentsAtLine(range).length > 0);660}661662private isEditorInlineOriginal(testEditor: ICodeEditor): boolean {663if (this.configurationService.getValue<boolean>('diffEditor.renderSideBySide')) {664return false;665}666667const foundEditor = this.editorService.visibleTextEditorControls.find(editor => {668if (editor.getEditorType() === EditorType.IDiffEditor) {669const diffEditor = editor as IDiffEditor;670return diffEditor.getOriginalEditor() === testEditor;671}672return false;673});674return !!foundEditor;675}676677private beginCompute(): Promise<void> {678this._computePromise = createCancelablePromise(token => {679const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri;680681if (editorURI) {682return this.commentService.getDocumentComments(editorURI);683}684685return Promise.resolve([]);686});687688this._computeAndSetPromise = this._computePromise.then(async commentInfos => {689await this.setComments(coalesce(commentInfos));690this._computePromise = null;691}, error => console.log(error));692this._computePromise.then(() => this._computeAndSetPromise = undefined);693return this._computeAndSetPromise;694}695696private beginComputeCommentingRanges() {697if (this._computeCommentingRangeScheduler) {698this._computeCommentingRangeScheduler.trigger(() => {699const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri;700701if (editorURI) {702return this.commentService.getDocumentComments(editorURI);703}704705return Promise.resolve([]);706}).then(commentInfos => {707if (this.commentService.isCommentingEnabled) {708const meaningfulCommentInfos = coalesce(commentInfos);709this._commentingRangeDecorator.update(this.editor, meaningfulCommentInfos, this.editor?.getPosition()?.lineNumber, this.editor?.getSelection() ?? undefined);710}711}, (err) => {712onUnexpectedError(err);713return null;714});715}716}717718public static get(editor: ICodeEditor): CommentController | null {719return editor.getContribution<CommentController>(ID);720}721722public revealCommentThread(threadId: string, commentUniqueId: number | undefined, fetchOnceIfNotExist: boolean, focus: CommentWidgetFocus): void {723const commentThreadWidget = this._commentWidgets.filter(widget => widget.commentThread.threadId === threadId);724if (commentThreadWidget.length === 1) {725commentThreadWidget[0].reveal(commentUniqueId, focus);726} else if (fetchOnceIfNotExist) {727if (this._computeAndSetPromise) {728this._computeAndSetPromise.then(_ => {729this.revealCommentThread(threadId, commentUniqueId, false, focus);730});731} else {732this.beginCompute().then(_ => {733this.revealCommentThread(threadId, commentUniqueId, false, focus);734});735}736}737}738739public collapseAll(): void {740for (const widget of this._commentWidgets) {741widget.collapse(true);742}743}744745public async collapseVisibleComments(): Promise<void> {746if (!this.editor) {747return;748}749const visibleRanges = this.editor.getVisibleRanges();750for (const widget of this._commentWidgets) {751if (widget.expanded && widget.commentThread.range) {752const isVisible = visibleRanges.some(visibleRange =>753Range.areIntersectingOrTouching(visibleRange, widget.commentThread.range!)754);755if (isVisible) {756await widget.collapse(true);757}758}759}760}761762private _updateCommentWidgetVisibleContext(): void {763const hasExpanded = this._commentWidgets.some(widget => widget.expanded);764this._commentWidgetVisible.set(hasExpanded);765}766767public expandAll(): void {768for (const widget of this._commentWidgets) {769widget.expand();770}771}772773public expandUnresolved(): void {774for (const widget of this._commentWidgets) {775if (widget.commentThread.state === languages.CommentThreadState.Unresolved) {776widget.expand();777}778}779}780781public nextCommentThread(focusThread: boolean): void {782this._findNearestCommentThread(focusThread);783}784785private _findNearestCommentThread(focusThread: boolean, reverse?: boolean): void {786if (!this._commentWidgets.length || !this.editor?.hasModel()) {787return;788}789790const after = reverse ? this.editor.getSelection().getStartPosition() : this.editor.getSelection().getEndPosition();791const sortedWidgets = this._commentWidgets.sort((a, b) => {792if (reverse) {793const temp = a;794a = b;795b = temp;796}797if (a.commentThread.range === undefined) {798return -1;799}800if (b.commentThread.range === undefined) {801return 1;802}803if (a.commentThread.range.startLineNumber < b.commentThread.range.startLineNumber) {804return -1;805}806807if (a.commentThread.range.startLineNumber > b.commentThread.range.startLineNumber) {808return 1;809}810811if (a.commentThread.range.startColumn < b.commentThread.range.startColumn) {812return -1;813}814815if (a.commentThread.range.startColumn > b.commentThread.range.startColumn) {816return 1;817}818819return 0;820});821822const idx = findFirstIdxMonotonousOrArrLen(sortedWidgets, widget => {823const lineValueOne = reverse ? after.lineNumber : (widget.commentThread.range?.startLineNumber ?? 0);824const lineValueTwo = reverse ? (widget.commentThread.range?.startLineNumber ?? 0) : after.lineNumber;825const columnValueOne = reverse ? after.column : (widget.commentThread.range?.startColumn ?? 0);826const columnValueTwo = reverse ? (widget.commentThread.range?.startColumn ?? 0) : after.column;827if (lineValueOne > lineValueTwo) {828return true;829}830831if (lineValueOne < lineValueTwo) {832return false;833}834835if (columnValueOne > columnValueTwo) {836return true;837}838return false;839});840841const nextWidget: ReviewZoneWidget | undefined = sortedWidgets[idx];842if (nextWidget !== undefined) {843this.editor.setSelection(nextWidget.commentThread.range ?? new Range(1, 1, 1, 1));844nextWidget.reveal(undefined, focusThread ? CommentWidgetFocus.Widget : CommentWidgetFocus.None);845}846}847848public previousCommentThread(focusThread: boolean): void {849this._findNearestCommentThread(focusThread, true);850}851852private _findNearestCommentingRange(reverse?: boolean): void {853if (!this.editor?.hasModel()) {854return;855}856857const after = this.editor.getSelection().getEndPosition();858const range = this._commentingRangeDecorator.getNearestCommentingRange(after, reverse);859if (range) {860const position = reverse ? range.getEndPosition() : range.getStartPosition();861this.editor.setPosition(position);862this.editor.revealLineInCenterIfOutsideViewport(position.lineNumber);863}864if (this.accessibilityService.isScreenReaderOptimized()) {865const commentRangeStart = range?.getStartPosition().lineNumber;866const commentRangeEnd = range?.getEndPosition().lineNumber;867if (commentRangeStart && commentRangeEnd) {868const oneLine = commentRangeStart === commentRangeEnd;869oneLine ? status(nls.localize('commentRange', "Line {0}", commentRangeStart)) : status(nls.localize('commentRangeStart', "Lines {0} to {1}", commentRangeStart, commentRangeEnd));870}871}872}873874public nextCommentingRange(): void {875this._findNearestCommentingRange();876}877878public previousCommentingRange(): void {879this._findNearestCommentingRange(true);880}881882public override dispose(): void {883super.dispose();884dispose(this._editorDisposables);885dispose(this._commentWidgets);886887this.editor = null!; // Strict null override - nulling out in dispose888}889890private onWillChangeModel(e: IModelChangedEvent): void {891if (e.newModelUrl) {892this.tryUpdateReservedSpace(e.newModelUrl);893}894}895896private async handleCommentAdded(editorId: string | undefined, uniqueOwner: string, thread: languages.AddedCommentThread): Promise<void> {897const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId);898if (matchedZones.length) {899return;900}901902const matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === uniqueOwner && zoneWidget.commentThread.commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range));903904if (matchedNewCommentThreadZones.length) {905matchedNewCommentThreadZones[0].update(thread);906return;907}908909const continueOnCommentIndex = this._inProcessContinueOnComments.get(uniqueOwner)?.findIndex(pending => {910if (pending.range === undefined) {911return thread.range === undefined;912} else {913return Range.lift(pending.range).equalsRange(thread.range);914}915});916let continueOnCommentText: string | undefined;917if ((continueOnCommentIndex !== undefined) && continueOnCommentIndex >= 0) {918continueOnCommentText = this._inProcessContinueOnComments.get(uniqueOwner)?.splice(continueOnCommentIndex, 1)[0].comment.body;919}920921const pendingCommentText = (this._pendingNewCommentCache[uniqueOwner] && this._pendingNewCommentCache[uniqueOwner][thread.threadId])922?? continueOnCommentText;923const pendingEdits = this._pendingEditsCache[uniqueOwner] && this._pendingEditsCache[uniqueOwner][thread.threadId];924const shouldReveal = thread.canReply && thread.isTemplate && (!thread.comments || (thread.comments.length === 0)) && (!thread.editorId || (thread.editorId === editorId));925await this.displayCommentThread(uniqueOwner, thread, shouldReveal, pendingCommentText, pendingEdits);926this._commentInfos.filter(info => info.uniqueOwner === uniqueOwner)[0].threads.push(thread);927this.tryUpdateReservedSpace();928}929930public onModelChanged(): void {931this.localToDispose.clear();932this.tryUpdateReservedSpace();933934this.removeCommentWidgetsAndStoreCache();935if (!this.editor) {936return;937}938939this._hasRespondedToEditorChange = false;940941this.localToDispose.add(this.editor.onMouseDown(e => this.onEditorMouseDown(e)));942this.localToDispose.add(this.editor.onMouseUp(e => this.onEditorMouseUp(e)));943if (this._editorDisposables.length) {944this.clearEditorListeners();945this.registerEditorListeners();946}947948this._computeCommentingRangeScheduler = new Delayer<ICommentInfo[]>(200);949this.localToDispose.add({950dispose: () => {951this._computeCommentingRangeScheduler?.cancel();952this._computeCommentingRangeScheduler = null;953}954});955this.localToDispose.add(this.editor.onDidChangeModelContent(async () => {956this.beginComputeCommentingRanges();957}));958this.localToDispose.add(this.commentService.onDidUpdateCommentThreads(async e => {959const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri;960if (!editorURI || !this.commentService.isCommentingEnabled) {961return;962}963964if (this._computePromise) {965await this._computePromise;966}967968const commentInfo = this._commentInfos.filter(info => info.uniqueOwner === e.uniqueOwner);969if (!commentInfo || !commentInfo.length) {970return;971}972973const added = e.added.filter(thread => thread.resource && thread.resource === editorURI.toString());974const removed = e.removed.filter(thread => thread.resource && thread.resource === editorURI.toString());975const changed = e.changed.filter(thread => thread.resource && thread.resource === editorURI.toString());976const pending = e.pending.filter(pending => pending.uri.toString() === editorURI.toString());977978removed.forEach(thread => {979const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId && zoneWidget.commentThread.threadId !== '');980if (matchedZones.length) {981const matchedZone = matchedZones[0];982const index = this._commentWidgets.indexOf(matchedZone);983this._commentWidgets.splice(index, 1);984matchedZone.dispose();985}986const infosThreads = this._commentInfos.filter(info => info.uniqueOwner === e.uniqueOwner)[0].threads;987for (let i = 0; i < infosThreads.length; i++) {988if (infosThreads[i] === thread) {989infosThreads.splice(i, 1);990i--;991}992}993});994995for (const thread of changed) {996const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId);997if (matchedZones.length) {998const matchedZone = matchedZones[0];999matchedZone.update(thread);1000this.openCommentsView(thread);1001}1002}1003const editorId = this.editor?.getId();1004for (const thread of added) {1005await this.handleCommentAdded(editorId, e.uniqueOwner, thread);1006}10071008for (const thread of pending) {1009await this.resumePendingComment(editorURI, thread);1010}1011this._commentThreadRangeDecorator.update(this.editor, commentInfo);1012}));10131014this.beginComputeAndHandleEditorChange();1015}10161017private async resumePendingComment(editorURI: URI, thread: languages.PendingCommentThread) {1018const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === thread.uniqueOwner && Range.lift(zoneWidget.commentThread.range)?.equalsRange(thread.range));1019if (thread.isReply && matchedZones.length) {1020this.commentService.removeContinueOnComment({ uniqueOwner: thread.uniqueOwner, uri: editorURI, range: thread.range, isReply: true });1021matchedZones[0].setPendingComment(thread.comment);1022} else if (matchedZones.length) {1023this.commentService.removeContinueOnComment({ uniqueOwner: thread.uniqueOwner, uri: editorURI, range: thread.range, isReply: false });1024const existingPendingComment = matchedZones[0].getPendingComments().newComment;1025// We need to try to reconcile the existing pending comment with the incoming pending comment1026let pendingComment: languages.PendingComment;1027if (!existingPendingComment || thread.comment.body.includes(existingPendingComment.body)) {1028pendingComment = thread.comment;1029} else if (existingPendingComment.body.includes(thread.comment.body)) {1030pendingComment = existingPendingComment;1031} else {1032pendingComment = { body: `${existingPendingComment}\n${thread.comment.body}`, cursor: thread.comment.cursor };1033}1034matchedZones[0].setPendingComment(pendingComment);1035} else if (!thread.isReply) {1036const threadStillAvailable = this.commentService.removeContinueOnComment({ uniqueOwner: thread.uniqueOwner, uri: editorURI, range: thread.range, isReply: false });1037if (!threadStillAvailable) {1038return;1039}1040if (!this._inProcessContinueOnComments.has(thread.uniqueOwner)) {1041this._inProcessContinueOnComments.set(thread.uniqueOwner, []);1042}1043this._inProcessContinueOnComments.get(thread.uniqueOwner)?.push(thread);1044await this.commentService.createCommentThreadTemplate(thread.uniqueOwner, thread.uri, thread.range ? Range.lift(thread.range) : undefined);1045}1046}10471048private beginComputeAndHandleEditorChange(): void {1049this.beginCompute().then(() => {1050if (!this._hasRespondedToEditorChange) {1051if (this._commentInfos.some(commentInfo => commentInfo.commentingRanges.ranges.length > 0 || commentInfo.commentingRanges.fileComments)) {1052this._hasRespondedToEditorChange = true;1053const verbose = this.configurationService.getValue(AccessibilityVerbositySettingId.Comments);1054if (verbose) {1055const keybinding = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getAriaLabel();1056if (keybinding) {1057status(nls.localize('hasCommentRangesKb', "Editor has commenting ranges, run the command Open Accessibility Help ({0}), for more information.", keybinding));1058} else {1059status(nls.localize('hasCommentRangesNoKb', "Editor has commenting ranges, run the command Open Accessibility Help, which is currently not triggerable via keybinding, for more information."));1060}1061} else {1062status(nls.localize('hasCommentRanges', "Editor has commenting ranges."));1063}1064}1065}1066});1067}10681069private async openCommentsView(thread: languages.CommentThread) {1070if (thread.comments && (thread.comments.length > 0) && threadHasMeaningfulComments(thread)) {1071const openViewState = this.configurationService.getValue<ICommentsConfiguration>(COMMENTS_SECTION).openView;1072if (openViewState === 'file') {1073return this.viewsService.openView(COMMENTS_VIEW_ID);1074} else if (openViewState === 'firstFile' || (openViewState === 'firstFileUnresolved' && thread.state === languages.CommentThreadState.Unresolved)) {1075const hasShownView = this.viewsService.getViewWithId<CommentsPanel>(COMMENTS_VIEW_ID)?.hasRendered;1076if (!hasShownView) {1077return this.viewsService.openView(COMMENTS_VIEW_ID);1078}1079}1080}1081return undefined;1082}10831084private async displayCommentThread(uniqueOwner: string, thread: languages.CommentThread, shouldReveal: boolean, pendingComment: languages.PendingComment | undefined, pendingEdits: { [key: number]: languages.PendingComment } | undefined): Promise<void> {1085const editor = this.editor?.getModel();1086if (!editor) {1087return;1088}1089if (!this.editor || this.isEditorInlineOriginal(this.editor)) {1090return;1091}10921093let continueOnCommentReply: languages.PendingCommentThread | undefined;1094if (thread.range && !pendingComment) {1095continueOnCommentReply = this.commentService.removeContinueOnComment({ uniqueOwner, uri: editor.uri, range: thread.range, isReply: true });1096}1097const zoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, uniqueOwner, thread, pendingComment ?? continueOnCommentReply?.comment, pendingEdits);1098await zoneWidget.display(thread.range, shouldReveal);1099this._commentWidgets.push(zoneWidget);1100this.localToDispose.add(zoneWidget.onDidChangeExpandedState(() => this._updateCommentWidgetVisibleContext()));1101this.localToDispose.add(zoneWidget.onDidClose(() => this._updateCommentWidgetVisibleContext()));1102this.openCommentsView(thread);1103}11041105private onEditorMouseDown(e: IEditorMouseEvent): void {1106this.mouseDownInfo = (e.target.element?.className.indexOf('comment-range-glyph') ?? -1) >= 0 ? parseMouseDownInfoFromEvent(e) : null;1107}11081109private onEditorMouseUp(e: IEditorMouseEvent): void {1110const matchedLineNumber = isMouseUpEventDragFromMouseDown(this.mouseDownInfo, e);1111this.mouseDownInfo = null;11121113if (!this.editor || matchedLineNumber === null || !e.target.element) {1114return;1115}1116const mouseUpIsOnDecorator = (e.target.element.className.indexOf('comment-range-glyph') >= 0);11171118const lineNumber = e.target.position!.lineNumber;1119let range: Range | undefined;1120let selection: Range | null | undefined;1121// Check for drag along gutter decoration1122if ((matchedLineNumber !== lineNumber)) {1123if (matchedLineNumber > lineNumber) {1124selection = new Range(matchedLineNumber, this.editor.getModel()!.getLineLength(matchedLineNumber) + 1, lineNumber, 1);1125} else {1126selection = new Range(matchedLineNumber, 1, lineNumber, this.editor.getModel()!.getLineLength(lineNumber) + 1);1127}1128} else if (mouseUpIsOnDecorator) {1129selection = this.editor.getSelection();1130}11311132// Check for selection at line number.1133if (selection && (selection.startLineNumber <= lineNumber) && (lineNumber <= selection.endLineNumber)) {1134range = selection;1135this.editor.setSelection(new Range(selection.endLineNumber, 1, selection.endLineNumber, 1));1136} else if (mouseUpIsOnDecorator) {1137range = new Range(lineNumber, 1, lineNumber, 1);1138}11391140if (range) {1141this.addOrToggleCommentAtLine(range, e);1142}1143}11441145public getCommentsAtLine(commentRange: Range | undefined): ReviewZoneWidget[] {1146return this._commentWidgets.filter(widget => widget.getGlyphPosition() === (commentRange ? commentRange.endLineNumber : 0));1147}11481149public async addOrToggleCommentAtLine(commentRange: Range | undefined, e: IEditorMouseEvent | undefined): Promise<void> {1150// If an add is already in progress, queue the next add and process it after the current one finishes to1151// prevent empty comment threads from being added to the same line.1152if (!this._addInProgress) {1153this._addInProgress = true;1154// The widget's position is undefined until the widget has been displayed, so rely on the glyph position instead1155const existingCommentsAtLine = this.getCommentsAtLine(commentRange);1156if (existingCommentsAtLine.length) {1157const allExpanded = existingCommentsAtLine.every(widget => widget.expanded);1158existingCommentsAtLine.forEach(allExpanded ? widget => widget.collapse(true) : widget => widget.expand(true));1159this.processNextThreadToAdd();1160return;1161} else {1162this.addCommentAtLine(commentRange, e);1163}1164} else {1165this._emptyThreadsToAddQueue.push([commentRange, e]);1166}1167}11681169private processNextThreadToAdd(): void {1170this._addInProgress = false;1171const info = this._emptyThreadsToAddQueue.shift();1172if (info) {1173this.addOrToggleCommentAtLine(info[0], info[1]);1174}1175}11761177private clipUserRangeToCommentRange(userRange: Range, commentRange: Range): Range {1178if (userRange.startLineNumber < commentRange.startLineNumber) {1179userRange = new Range(commentRange.startLineNumber, commentRange.startColumn, userRange.endLineNumber, userRange.endColumn);1180}1181if (userRange.endLineNumber > commentRange.endLineNumber) {1182userRange = new Range(userRange.startLineNumber, userRange.startColumn, commentRange.endLineNumber, commentRange.endColumn);1183}1184return userRange;1185}11861187public addCommentAtLine(range: Range | undefined, e: IEditorMouseEvent | undefined): Promise<void> {1188const newCommentInfos = this._commentingRangeDecorator.getMatchedCommentAction(range);1189if (!newCommentInfos.length || !this.editor?.hasModel()) {1190this._addInProgress = false;1191if (!newCommentInfos.length) {1192if (range) {1193this.notificationService.error(nls.localize('comments.addCommand.error', "The cursor must be within a commenting range to add a comment."));1194} else {1195this.notificationService.error(nls.localize('comments.addFileCommentCommand.error', "File comments are not allowed on this file."));1196}1197}1198return Promise.resolve();1199}12001201if (newCommentInfos.length > 1) {1202if (e && range) {1203this.contextMenuService.showContextMenu({1204getAnchor: () => e.event,1205getActions: () => this.getContextMenuActions(newCommentInfos, range),1206getActionsContext: () => newCommentInfos.length ? newCommentInfos[0] : undefined,1207onHide: () => { this._addInProgress = false; }1208});12091210return Promise.resolve();1211} else {1212const picks = this.getCommentProvidersQuickPicks(newCommentInfos);1213return this.quickInputService.pick(picks, { placeHolder: nls.localize('pickCommentService', "Select Comment Provider"), matchOnDescription: true }).then(pick => {1214if (!pick) {1215return;1216}12171218const commentInfos = newCommentInfos.filter(info => info.action.ownerId === pick.id);12191220if (commentInfos.length) {1221const { ownerId } = commentInfos[0].action;1222const clippedRange = range && commentInfos[0].range ? this.clipUserRangeToCommentRange(range, commentInfos[0].range) : range;1223this.addCommentAtLine2(clippedRange, ownerId);1224}1225}).then(() => {1226this._addInProgress = false;1227});1228}1229} else {1230const { ownerId } = newCommentInfos[0].action;1231const clippedRange = range && newCommentInfos[0].range ? this.clipUserRangeToCommentRange(range, newCommentInfos[0].range) : range;1232this.addCommentAtLine2(clippedRange, ownerId);1233}12341235return Promise.resolve();1236}12371238private getCommentProvidersQuickPicks(commentInfos: MergedCommentRangeActions[]) {1239const picks: QuickPickInput[] = commentInfos.map((commentInfo) => {1240const { ownerId, extensionId, label } = commentInfo.action;12411242return {1243label: label ?? extensionId ?? ownerId,1244id: ownerId1245} satisfies IQuickPickItem;1246});12471248return picks;1249}12501251private getContextMenuActions(commentInfos: MergedCommentRangeActions[], commentRange: Range): IAction[] {1252const actions: IAction[] = [];12531254commentInfos.forEach(commentInfo => {1255const { ownerId, extensionId, label } = commentInfo.action;12561257actions.push(new Action(1258'addCommentThread',1259`${label || extensionId}`,1260undefined,1261true,1262() => {1263const clippedRange = commentInfo.range ? this.clipUserRangeToCommentRange(commentRange, commentInfo.range) : commentRange;1264this.addCommentAtLine2(clippedRange, ownerId);1265return Promise.resolve();1266}1267));1268});1269return actions;1270}12711272public addCommentAtLine2(range: Range | undefined, ownerId: string) {1273if (!this.editor) {1274return;1275}1276this.commentService.createCommentThreadTemplate(ownerId, this.editor.getModel()!.uri, range, this.editor.getId());1277this.processNextThreadToAdd();1278return;1279}12801281private getExistingCommentEditorOptions(editor: ICodeEditor) {1282const lineDecorationsWidth: number = editor.getOption(EditorOption.lineDecorationsWidth);1283let extraEditorClassName: string[] = [];1284const configuredExtraClassName = editor.getRawOptions().extraEditorClassName;1285if (configuredExtraClassName) {1286extraEditorClassName = configuredExtraClassName.split(' ');1287}1288return { lineDecorationsWidth, extraEditorClassName };1289}12901291private getWithoutCommentsEditorOptions(editor: ICodeEditor, extraEditorClassName: string[], startingLineDecorationsWidth: number) {1292let lineDecorationsWidth = startingLineDecorationsWidth;1293const inlineCommentPos = extraEditorClassName.findIndex(name => name === 'inline-comment');1294if (inlineCommentPos >= 0) {1295extraEditorClassName.splice(inlineCommentPos, 1);1296}12971298const options = editor.getOptions();1299if (options.get(EditorOption.folding) && options.get(EditorOption.showFoldingControls) !== 'never') {1300lineDecorationsWidth += 11; // 11 comes from https://github.com/microsoft/vscode/blob/94ee5f58619d59170983f453fe78f156c0cc73a3/src/vs/workbench/contrib/comments/browser/media/review.css#L4851301}1302lineDecorationsWidth -= 24;1303return { extraEditorClassName, lineDecorationsWidth };1304}13051306private getWithCommentsLineDecorationWidth(editor: ICodeEditor, startingLineDecorationsWidth: number) {1307let lineDecorationsWidth = startingLineDecorationsWidth;1308const options = editor.getOptions();1309if (options.get(EditorOption.folding) && options.get(EditorOption.showFoldingControls) !== 'never') {1310lineDecorationsWidth -= 11;1311}1312lineDecorationsWidth += 24;1313this._commentingRangeAmountReserved = lineDecorationsWidth;1314return this._commentingRangeAmountReserved;1315}13161317private getWithCommentsEditorOptions(editor: ICodeEditor, extraEditorClassName: string[], startingLineDecorationsWidth: number) {1318extraEditorClassName.push('inline-comment');1319return { lineDecorationsWidth: this.getWithCommentsLineDecorationWidth(editor, startingLineDecorationsWidth), extraEditorClassName };1320}13211322private updateEditorLayoutOptions(editor: ICodeEditor, extraEditorClassName: string[], lineDecorationsWidth: number) {1323editor.updateOptions({1324extraEditorClassName: extraEditorClassName.join(' '),1325lineDecorationsWidth: lineDecorationsWidth1326});1327}13281329private ensureCommentingRangeReservedAmount(editor: ICodeEditor) {1330const existing = this.getExistingCommentEditorOptions(editor);1331if (existing.lineDecorationsWidth !== this._commentingRangeAmountReserved) {1332editor.updateOptions({1333lineDecorationsWidth: this.getWithCommentsLineDecorationWidth(editor, existing.lineDecorationsWidth)1334});1335}1336}13371338private tryUpdateReservedSpace(uri?: URI) {1339if (!this.editor) {1340return;1341}13421343const hasCommentsOrRangesInInfo = this._commentInfos.some(info => {1344const hasRanges = Boolean(info.commentingRanges && (Array.isArray(info.commentingRanges) ? info.commentingRanges : info.commentingRanges.ranges).length);1345return hasRanges || (info.threads.length > 0);1346});1347uri = uri ?? this.editor.getModel()?.uri;1348const resourceHasCommentingRanges = uri ? this.commentService.resourceHasCommentingRanges(uri) : false;13491350const hasCommentsOrRanges = hasCommentsOrRangesInInfo || resourceHasCommentingRanges;13511352if (hasCommentsOrRanges && this.commentService.isCommentingEnabled) {1353if (!this._commentingRangeSpaceReserved) {1354this._commentingRangeSpaceReserved = true;1355const { lineDecorationsWidth, extraEditorClassName } = this.getExistingCommentEditorOptions(this.editor);1356const newOptions = this.getWithCommentsEditorOptions(this.editor, extraEditorClassName, lineDecorationsWidth);1357this.updateEditorLayoutOptions(this.editor, newOptions.extraEditorClassName, newOptions.lineDecorationsWidth);1358} else {1359this.ensureCommentingRangeReservedAmount(this.editor);1360}1361} else if ((!hasCommentsOrRanges || !this.commentService.isCommentingEnabled) && this._commentingRangeSpaceReserved) {1362this._commentingRangeSpaceReserved = false;1363const { lineDecorationsWidth, extraEditorClassName } = this.getExistingCommentEditorOptions(this.editor);1364const newOptions = this.getWithoutCommentsEditorOptions(this.editor, extraEditorClassName, lineDecorationsWidth);1365this.updateEditorLayoutOptions(this.editor, newOptions.extraEditorClassName, newOptions.lineDecorationsWidth);1366}1367}13681369private async setComments(commentInfos: ICommentInfo[]): Promise<void> {1370if (!this.editor || !this.commentService.isCommentingEnabled) {1371return;1372}13731374this._commentInfos = commentInfos;1375this.tryUpdateReservedSpace();1376// create viewzones1377this.removeCommentWidgetsAndStoreCache();13781379let hasCommentingRanges = false;1380for (const info of this._commentInfos) {1381if (!hasCommentingRanges && (info.commentingRanges.ranges.length > 0 || info.commentingRanges.fileComments)) {1382hasCommentingRanges = true;1383}13841385const providerCacheStore = this._pendingNewCommentCache[info.uniqueOwner];1386const providerEditsCacheStore = this._pendingEditsCache[info.uniqueOwner];1387info.threads = info.threads.filter(thread => !thread.isDisposed);1388for (const thread of info.threads) {1389let pendingComment: languages.PendingComment | undefined = undefined;1390if (providerCacheStore) {1391pendingComment = providerCacheStore[thread.threadId];1392}13931394let pendingEdits: { [key: number]: languages.PendingComment } | undefined = undefined;1395if (providerEditsCacheStore) {1396pendingEdits = providerEditsCacheStore[thread.threadId];1397}13981399await this.displayCommentThread(info.uniqueOwner, thread, false, pendingComment, pendingEdits);1400}1401for (const thread of info.pendingCommentThreads ?? []) {1402this.resumePendingComment(this.editor.getModel()!.uri, thread);1403}1404}14051406this._commentingRangeDecorator.update(this.editor, this._commentInfos);1407this._commentThreadRangeDecorator.update(this.editor, this._commentInfos);14081409if (hasCommentingRanges) {1410this._activeEditorHasCommentingRange.set(true);1411} else {1412this._activeEditorHasCommentingRange.set(false);1413}1414}14151416public collapseAndFocusRange(threadId: string): void {1417this._commentWidgets?.find(widget => widget.commentThread.threadId === threadId)?.collapseAndFocusRange();1418}14191420private removeCommentWidgetsAndStoreCache() {1421if (this._commentWidgets) {1422this._commentWidgets.forEach(zone => {1423const pendingComments = zone.getPendingComments();1424const pendingNewComment = pendingComments.newComment;1425const providerNewCommentCacheStore = this._pendingNewCommentCache[zone.uniqueOwner];14261427let lastCommentBody;1428if (zone.commentThread.comments && zone.commentThread.comments.length) {1429const lastComment = zone.commentThread.comments[zone.commentThread.comments.length - 1];1430if (typeof lastComment.body === 'string') {1431lastCommentBody = lastComment.body;1432} else {1433lastCommentBody = lastComment.body.value;1434}1435}1436if (pendingNewComment && (pendingNewComment.body !== lastCommentBody)) {1437if (!providerNewCommentCacheStore) {1438this._pendingNewCommentCache[zone.uniqueOwner] = {};1439}14401441this._pendingNewCommentCache[zone.uniqueOwner][zone.commentThread.threadId] = pendingNewComment;1442} else {1443if (providerNewCommentCacheStore) {1444delete providerNewCommentCacheStore[zone.commentThread.threadId];1445}1446}14471448const pendingEdits = pendingComments.edits;1449const providerEditsCacheStore = this._pendingEditsCache[zone.uniqueOwner];1450if (Object.keys(pendingEdits).length > 0) {1451if (!providerEditsCacheStore) {1452this._pendingEditsCache[zone.uniqueOwner] = {};1453}1454this._pendingEditsCache[zone.uniqueOwner][zone.commentThread.threadId] = pendingEdits;1455} else if (providerEditsCacheStore) {1456delete providerEditsCacheStore[zone.commentThread.threadId];1457}14581459zone.dispose();1460});1461}14621463this._commentWidgets = [];1464}1465}146614671468