Path: blob/main/src/vs/workbench/contrib/comments/browser/commentsController.ts
3296 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 { 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.commentingRangeDecorations = [];368}369}370371/**372* Navigate to the next or previous comment in the current thread.373* @param type374*/375export function moveToNextCommentInThread(commentInfo: { thread: languages.CommentThread<IRange>; comment?: languages.Comment } | undefined, type: 'next' | 'previous') {376if (!commentInfo?.comment || !commentInfo?.thread?.comments) {377return;378}379const currentIndex = commentInfo.thread.comments?.indexOf(commentInfo.comment);380if (currentIndex === undefined || currentIndex < 0) {381return;382}383if (type === 'previous' && currentIndex === 0) {384return;385}386if (type === 'next' && currentIndex === commentInfo.thread.comments.length - 1) {387return;388}389const comment = commentInfo.thread.comments?.[type === 'previous' ? currentIndex - 1 : currentIndex + 1];390if (!comment) {391return;392}393return {394...commentInfo,395comment,396};397}398399export function revealCommentThread(commentService: ICommentService, editorService: IEditorService, uriIdentityService: IUriIdentityService,400commentThread: languages.CommentThread<IRange>, comment: languages.Comment | undefined, focusReply?: boolean, pinned?: boolean, preserveFocus?: boolean, sideBySide?: boolean): void {401if (!commentThread.resource) {402return;403}404if (!commentService.isCommentingEnabled) {405commentService.enableCommenting(true);406}407408const range = commentThread.range;409const focus = focusReply ? CommentWidgetFocus.Editor : (preserveFocus ? CommentWidgetFocus.None : CommentWidgetFocus.Widget);410411const activeEditor = editorService.activeTextEditorControl;412// If the active editor is a diff editor where one of the sides has the comment,413// then we try to reveal the comment in the diff editor.414const currentActiveResources: IEditor[] = isDiffEditor(activeEditor) ? [activeEditor.getOriginalEditor(), activeEditor.getModifiedEditor()]415: (activeEditor ? [activeEditor] : []);416const threadToReveal = commentThread.threadId;417const commentToReveal = comment?.uniqueIdInThread;418const resource = URI.parse(commentThread.resource);419420for (const editor of currentActiveResources) {421const model = editor.getModel();422if ((model instanceof TextModel) && uriIdentityService.extUri.isEqual(resource, model.uri)) {423424if (threadToReveal && isCodeEditor(editor)) {425const controller = CommentController.get(editor);426controller?.revealCommentThread(threadToReveal, commentToReveal, true, focus);427}428return;429}430}431432editorService.openEditor({433resource,434options: {435pinned: pinned,436preserveFocus: preserveFocus,437selection: range ?? new Range(1, 1, 1, 1)438}439}, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => {440if (editor) {441const control = editor.getControl();442if (threadToReveal && isCodeEditor(control)) {443const controller = CommentController.get(control);444controller?.revealCommentThread(threadToReveal, commentToReveal, true, focus);445}446}447});448}449450export class CommentController implements IEditorContribution {451private readonly globalToDispose = new DisposableStore();452private readonly localToDispose = 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 _computeCommentingRangePromise!: CancelablePromise<ICommentInfo[]> | null;466private _computeCommentingRangeScheduler!: Delayer<Array<ICommentInfo | null>> | null;467private _pendingNewCommentCache: { [key: string]: { [key: string]: languages.PendingComment } };468private _pendingEditsCache: { [key: string]: { [key: string]: { [key: number]: languages.PendingComment } } }; // uniqueOwner -> threadId -> uniqueIdInThread -> pending comment469private _inProcessContinueOnComments: Map<string, languages.PendingCommentThread[]> = new Map();470private _editorDisposables: IDisposable[] = [];471private _activeCursorHasCommentingRange: IContextKey<boolean>;472private _activeCursorHasComment: IContextKey<boolean>;473private _activeEditorHasCommentingRange: 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) {491this._commentInfos = [];492this._commentWidgets = [];493this._pendingNewCommentCache = {};494this._pendingEditsCache = {};495this._computePromise = null;496this._activeCursorHasCommentingRange = CommentContextKeys.activeCursorHasCommentingRange.bindTo(contextKeyService);497this._activeCursorHasComment = CommentContextKeys.activeCursorHasComment.bindTo(contextKeyService);498this._activeEditorHasCommentingRange = CommentContextKeys.activeEditorHasCommentingRange.bindTo(contextKeyService);499500if (editor instanceof EmbeddedCodeEditorWidget) {501return;502}503504this.editor = editor;505506this._commentingRangeDecorator = new CommentingRangeDecorator();507this.globalToDispose.add(this._commentingRangeDecorator.onDidChangeDecorationsCount(count => {508if (count === 0) {509this.clearEditorListeners();510} else if (this._editorDisposables.length === 0) {511this.registerEditorListeners();512}513}));514515this.globalToDispose.add(this._commentThreadRangeDecorator = new CommentThreadRangeDecorator(this.commentService));516517this.globalToDispose.add(this.commentService.onDidDeleteDataProvider(ownerId => {518if (ownerId) {519delete this._pendingNewCommentCache[ownerId];520delete this._pendingEditsCache[ownerId];521} else {522this._pendingNewCommentCache = {};523this._pendingEditsCache = {};524}525this.beginCompute();526}));527this.globalToDispose.add(this.commentService.onDidSetDataProvider(_ => this.beginComputeAndHandleEditorChange()));528this.globalToDispose.add(this.commentService.onDidUpdateCommentingRanges(_ => this.beginComputeAndHandleEditorChange()));529530this.globalToDispose.add(this.commentService.onDidSetResourceCommentInfos(async e => {531const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri;532if (editorURI && editorURI.toString() === e.resource.toString()) {533await this.setComments(e.commentInfos.filter(commentInfo => commentInfo !== null));534}535}));536537this.globalToDispose.add(this.commentService.onDidChangeCommentingEnabled(e => {538if (e) {539this.registerEditorListeners();540this.beginCompute();541} else {542this.tryUpdateReservedSpace();543this.clearEditorListeners();544this._commentingRangeDecorator.update(this.editor, []);545this._commentThreadRangeDecorator.update(this.editor, []);546dispose(this._commentWidgets);547this._commentWidgets = [];548}549}));550551this.globalToDispose.add(this.editor.onWillChangeModel(e => this.onWillChangeModel(e)));552this.globalToDispose.add(this.editor.onDidChangeModel(_ => this.onModelChanged()));553this.globalToDispose.add(this.configurationService.onDidChangeConfiguration(e => {554if (e.affectsConfiguration('diffEditor.renderSideBySide')) {555this.beginCompute();556}557}));558559this.onModelChanged();560this.codeEditorService.registerDecorationType('comment-controller', COMMENTEDITOR_DECORATION_KEY, {});561this.globalToDispose.add(562this.commentService.registerContinueOnCommentProvider({563provideContinueOnComments: () => {564const pendingComments: languages.PendingCommentThread[] = [];565if (this._commentWidgets) {566for (const zone of this._commentWidgets) {567const zonePendingComments = zone.getPendingComments();568const pendingNewComment = zonePendingComments.newComment;569if (!pendingNewComment) {570continue;571}572let lastCommentBody;573if (zone.commentThread.comments && zone.commentThread.comments.length) {574const lastComment = zone.commentThread.comments[zone.commentThread.comments.length - 1];575if (typeof lastComment.body === 'string') {576lastCommentBody = lastComment.body;577} else {578lastCommentBody = lastComment.body.value;579}580}581582if (pendingNewComment.body !== lastCommentBody) {583pendingComments.push({584uniqueOwner: zone.uniqueOwner,585uri: zone.editor.getModel()!.uri,586range: zone.commentThread.range,587comment: pendingNewComment,588isReply: (zone.commentThread.comments !== undefined) && (zone.commentThread.comments.length > 0)589});590}591}592}593return pendingComments;594}595})596);597598}599600private registerEditorListeners() {601this._editorDisposables = [];602if (!this.editor) {603return;604}605this._editorDisposables.push(this.editor.onMouseMove(e => this.onEditorMouseMove(e)));606this._editorDisposables.push(this.editor.onMouseLeave(() => this.onEditorMouseLeave()));607this._editorDisposables.push(this.editor.onDidChangeCursorPosition(e => this.onEditorChangeCursorPosition(e.position)));608this._editorDisposables.push(this.editor.onDidFocusEditorWidget(() => this.onEditorChangeCursorPosition(this.editor?.getPosition() ?? null)));609this._editorDisposables.push(this.editor.onDidChangeCursorSelection(e => this.onEditorChangeCursorSelection(e)));610this._editorDisposables.push(this.editor.onDidBlurEditorWidget(() => this.onEditorChangeCursorSelection()));611}612613private clearEditorListeners() {614dispose(this._editorDisposables);615this._editorDisposables = [];616}617618private onEditorMouseLeave() {619this._commentingRangeDecorator.updateHover();620}621622private onEditorMouseMove(e: IEditorMouseEvent): void {623const position = e.target.position?.lineNumber;624if (e.event.leftButton.valueOf() && position && this.mouseDownInfo) {625this._commentingRangeDecorator.updateSelection(position, new Range(this.mouseDownInfo.lineNumber, 1, position, 1));626} else {627this._commentingRangeDecorator.updateHover(position);628}629}630631private onEditorChangeCursorSelection(e?: ICursorSelectionChangedEvent): void {632const position = this.editor?.getPosition()?.lineNumber;633if (position) {634this._commentingRangeDecorator.updateSelection(position, e?.selection);635}636}637638private onEditorChangeCursorPosition(e: Position | null) {639if (!e) {640return;641}642const range = Range.fromPositions(e, { column: -1, lineNumber: e.lineNumber });643const decorations = this.editor?.getDecorationsInRange(range);644let hasCommentingRange = false;645if (decorations) {646for (const decoration of decorations) {647if (decoration.options.description === CommentGlyphWidget.description) {648// We don't allow multiple comments on the same line.649hasCommentingRange = false;650break;651} else if (decoration.options.description === CommentingRangeDecorator.description) {652hasCommentingRange = true;653}654}655}656this._activeCursorHasCommentingRange.set(hasCommentingRange);657this._activeCursorHasComment.set(this.getCommentsAtLine(range).length > 0);658}659660private isEditorInlineOriginal(testEditor: ICodeEditor): boolean {661if (this.configurationService.getValue<boolean>('diffEditor.renderSideBySide')) {662return false;663}664665const foundEditor = this.editorService.visibleTextEditorControls.find(editor => {666if (editor.getEditorType() === EditorType.IDiffEditor) {667const diffEditor = editor as IDiffEditor;668return diffEditor.getOriginalEditor() === testEditor;669}670return false;671});672return !!foundEditor;673}674675private beginCompute(): Promise<void> {676this._computePromise = createCancelablePromise(token => {677const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri;678679if (editorURI) {680return this.commentService.getDocumentComments(editorURI);681}682683return Promise.resolve([]);684});685686this._computeAndSetPromise = this._computePromise.then(async commentInfos => {687await this.setComments(coalesce(commentInfos));688this._computePromise = null;689}, error => console.log(error));690this._computePromise.then(() => this._computeAndSetPromise = undefined);691return this._computeAndSetPromise;692}693694private beginComputeCommentingRanges() {695if (this._computeCommentingRangeScheduler) {696if (this._computeCommentingRangePromise) {697this._computeCommentingRangePromise.cancel();698this._computeCommentingRangePromise = null;699}700701this._computeCommentingRangeScheduler.trigger(() => {702const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri;703704if (editorURI) {705return this.commentService.getDocumentComments(editorURI);706}707708return Promise.resolve([]);709}).then(commentInfos => {710if (this.commentService.isCommentingEnabled) {711const meaningfulCommentInfos = coalesce(commentInfos);712this._commentingRangeDecorator.update(this.editor, meaningfulCommentInfos, this.editor?.getPosition()?.lineNumber, this.editor?.getSelection() ?? undefined);713}714}, (err) => {715onUnexpectedError(err);716return null;717});718}719}720721public static get(editor: ICodeEditor): CommentController | null {722return editor.getContribution<CommentController>(ID);723}724725public revealCommentThread(threadId: string, commentUniqueId: number | undefined, fetchOnceIfNotExist: boolean, focus: CommentWidgetFocus): void {726const commentThreadWidget = this._commentWidgets.filter(widget => widget.commentThread.threadId === threadId);727if (commentThreadWidget.length === 1) {728commentThreadWidget[0].reveal(commentUniqueId, focus);729} else if (fetchOnceIfNotExist) {730if (this._computeAndSetPromise) {731this._computeAndSetPromise.then(_ => {732this.revealCommentThread(threadId, commentUniqueId, false, focus);733});734} else {735this.beginCompute().then(_ => {736this.revealCommentThread(threadId, commentUniqueId, false, focus);737});738}739}740}741742public collapseAll(): void {743for (const widget of this._commentWidgets) {744widget.collapse(true);745}746}747748public expandAll(): void {749for (const widget of this._commentWidgets) {750widget.expand();751}752}753754public expandUnresolved(): void {755for (const widget of this._commentWidgets) {756if (widget.commentThread.state === languages.CommentThreadState.Unresolved) {757widget.expand();758}759}760}761762public nextCommentThread(focusThread: boolean): void {763this._findNearestCommentThread(focusThread);764}765766private _findNearestCommentThread(focusThread: boolean, reverse?: boolean): void {767if (!this._commentWidgets.length || !this.editor?.hasModel()) {768return;769}770771const after = reverse ? this.editor.getSelection().getStartPosition() : this.editor.getSelection().getEndPosition();772const sortedWidgets = this._commentWidgets.sort((a, b) => {773if (reverse) {774const temp = a;775a = b;776b = temp;777}778if (a.commentThread.range === undefined) {779return -1;780}781if (b.commentThread.range === undefined) {782return 1;783}784if (a.commentThread.range.startLineNumber < b.commentThread.range.startLineNumber) {785return -1;786}787788if (a.commentThread.range.startLineNumber > b.commentThread.range.startLineNumber) {789return 1;790}791792if (a.commentThread.range.startColumn < b.commentThread.range.startColumn) {793return -1;794}795796if (a.commentThread.range.startColumn > b.commentThread.range.startColumn) {797return 1;798}799800return 0;801});802803const idx = findFirstIdxMonotonousOrArrLen(sortedWidgets, widget => {804const lineValueOne = reverse ? after.lineNumber : (widget.commentThread.range?.startLineNumber ?? 0);805const lineValueTwo = reverse ? (widget.commentThread.range?.startLineNumber ?? 0) : after.lineNumber;806const columnValueOne = reverse ? after.column : (widget.commentThread.range?.startColumn ?? 0);807const columnValueTwo = reverse ? (widget.commentThread.range?.startColumn ?? 0) : after.column;808if (lineValueOne > lineValueTwo) {809return true;810}811812if (lineValueOne < lineValueTwo) {813return false;814}815816if (columnValueOne > columnValueTwo) {817return true;818}819return false;820});821822const nextWidget: ReviewZoneWidget | undefined = sortedWidgets[idx];823if (nextWidget !== undefined) {824this.editor.setSelection(nextWidget.commentThread.range ?? new Range(1, 1, 1, 1));825nextWidget.reveal(undefined, focusThread ? CommentWidgetFocus.Widget : CommentWidgetFocus.None);826}827}828829public previousCommentThread(focusThread: boolean): void {830this._findNearestCommentThread(focusThread, true);831}832833private _findNearestCommentingRange(reverse?: boolean): void {834if (!this.editor?.hasModel()) {835return;836}837838const after = this.editor.getSelection().getEndPosition();839const range = this._commentingRangeDecorator.getNearestCommentingRange(after, reverse);840if (range) {841const position = reverse ? range.getEndPosition() : range.getStartPosition();842this.editor.setPosition(position);843this.editor.revealLineInCenterIfOutsideViewport(position.lineNumber);844}845if (this.accessibilityService.isScreenReaderOptimized()) {846const commentRangeStart = range?.getStartPosition().lineNumber;847const commentRangeEnd = range?.getEndPosition().lineNumber;848if (commentRangeStart && commentRangeEnd) {849const oneLine = commentRangeStart === commentRangeEnd;850oneLine ? status(nls.localize('commentRange', "Line {0}", commentRangeStart)) : status(nls.localize('commentRangeStart', "Lines {0} to {1}", commentRangeStart, commentRangeEnd));851}852}853}854855public nextCommentingRange(): void {856this._findNearestCommentingRange();857}858859public previousCommentingRange(): void {860this._findNearestCommentingRange(true);861}862863public dispose(): void {864this.globalToDispose.dispose();865this.localToDispose.dispose();866dispose(this._editorDisposables);867dispose(this._commentWidgets);868869this.editor = null!; // Strict null override - nulling out in dispose870}871872private onWillChangeModel(e: IModelChangedEvent): void {873if (e.newModelUrl) {874this.tryUpdateReservedSpace(e.newModelUrl);875}876}877878private async handleCommentAdded(editorId: string | undefined, uniqueOwner: string, thread: languages.AddedCommentThread): Promise<void> {879const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId);880if (matchedZones.length) {881return;882}883884const matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === uniqueOwner && zoneWidget.commentThread.commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range));885886if (matchedNewCommentThreadZones.length) {887matchedNewCommentThreadZones[0].update(thread);888return;889}890891const continueOnCommentIndex = this._inProcessContinueOnComments.get(uniqueOwner)?.findIndex(pending => {892if (pending.range === undefined) {893return thread.range === undefined;894} else {895return Range.lift(pending.range).equalsRange(thread.range);896}897});898let continueOnCommentText: string | undefined;899if ((continueOnCommentIndex !== undefined) && continueOnCommentIndex >= 0) {900continueOnCommentText = this._inProcessContinueOnComments.get(uniqueOwner)?.splice(continueOnCommentIndex, 1)[0].comment.body;901}902903const pendingCommentText = (this._pendingNewCommentCache[uniqueOwner] && this._pendingNewCommentCache[uniqueOwner][thread.threadId])904?? continueOnCommentText;905const pendingEdits = this._pendingEditsCache[uniqueOwner] && this._pendingEditsCache[uniqueOwner][thread.threadId];906const shouldReveal = thread.canReply && thread.isTemplate && (!thread.comments || (thread.comments.length === 0)) && (!thread.editorId || (thread.editorId === editorId));907await this.displayCommentThread(uniqueOwner, thread, shouldReveal, pendingCommentText, pendingEdits);908this._commentInfos.filter(info => info.uniqueOwner === uniqueOwner)[0].threads.push(thread);909this.tryUpdateReservedSpace();910}911912public onModelChanged(): void {913this.localToDispose.clear();914this.tryUpdateReservedSpace();915916this.removeCommentWidgetsAndStoreCache();917if (!this.editor) {918return;919}920921this._hasRespondedToEditorChange = false;922923this.localToDispose.add(this.editor.onMouseDown(e => this.onEditorMouseDown(e)));924this.localToDispose.add(this.editor.onMouseUp(e => this.onEditorMouseUp(e)));925if (this._editorDisposables.length) {926this.clearEditorListeners();927this.registerEditorListeners();928}929930this._computeCommentingRangeScheduler = new Delayer<ICommentInfo[]>(200);931this.localToDispose.add({932dispose: () => {933this._computeCommentingRangeScheduler?.cancel();934this._computeCommentingRangeScheduler = null;935}936});937this.localToDispose.add(this.editor.onDidChangeModelContent(async () => {938this.beginComputeCommentingRanges();939}));940this.localToDispose.add(this.commentService.onDidUpdateCommentThreads(async e => {941const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri;942if (!editorURI || !this.commentService.isCommentingEnabled) {943return;944}945946if (this._computePromise) {947await this._computePromise;948}949950const commentInfo = this._commentInfos.filter(info => info.uniqueOwner === e.uniqueOwner);951if (!commentInfo || !commentInfo.length) {952return;953}954955const added = e.added.filter(thread => thread.resource && thread.resource === editorURI.toString());956const removed = e.removed.filter(thread => thread.resource && thread.resource === editorURI.toString());957const changed = e.changed.filter(thread => thread.resource && thread.resource === editorURI.toString());958const pending = e.pending.filter(pending => pending.uri.toString() === editorURI.toString());959960removed.forEach(thread => {961const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId && zoneWidget.commentThread.threadId !== '');962if (matchedZones.length) {963const matchedZone = matchedZones[0];964const index = this._commentWidgets.indexOf(matchedZone);965this._commentWidgets.splice(index, 1);966matchedZone.dispose();967}968const infosThreads = this._commentInfos.filter(info => info.uniqueOwner === e.uniqueOwner)[0].threads;969for (let i = 0; i < infosThreads.length; i++) {970if (infosThreads[i] === thread) {971infosThreads.splice(i, 1);972i--;973}974}975});976977for (const thread of changed) {978const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId);979if (matchedZones.length) {980const matchedZone = matchedZones[0];981matchedZone.update(thread);982this.openCommentsView(thread);983}984}985const editorId = this.editor?.getId();986for (const thread of added) {987await this.handleCommentAdded(editorId, e.uniqueOwner, thread);988}989990for (const thread of pending) {991await this.resumePendingComment(editorURI, thread);992}993this._commentThreadRangeDecorator.update(this.editor, commentInfo);994}));995996this.beginComputeAndHandleEditorChange();997}998999private async resumePendingComment(editorURI: URI, thread: languages.PendingCommentThread) {1000const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === thread.uniqueOwner && Range.lift(zoneWidget.commentThread.range)?.equalsRange(thread.range));1001if (thread.isReply && matchedZones.length) {1002this.commentService.removeContinueOnComment({ uniqueOwner: thread.uniqueOwner, uri: editorURI, range: thread.range, isReply: true });1003matchedZones[0].setPendingComment(thread.comment);1004} else if (matchedZones.length) {1005this.commentService.removeContinueOnComment({ uniqueOwner: thread.uniqueOwner, uri: editorURI, range: thread.range, isReply: false });1006const existingPendingComment = matchedZones[0].getPendingComments().newComment;1007// We need to try to reconcile the existing pending comment with the incoming pending comment1008let pendingComment: languages.PendingComment;1009if (!existingPendingComment || thread.comment.body.includes(existingPendingComment.body)) {1010pendingComment = thread.comment;1011} else if (existingPendingComment.body.includes(thread.comment.body)) {1012pendingComment = existingPendingComment;1013} else {1014pendingComment = { body: `${existingPendingComment}\n${thread.comment.body}`, cursor: thread.comment.cursor };1015}1016matchedZones[0].setPendingComment(pendingComment);1017} else if (!thread.isReply) {1018const threadStillAvailable = this.commentService.removeContinueOnComment({ uniqueOwner: thread.uniqueOwner, uri: editorURI, range: thread.range, isReply: false });1019if (!threadStillAvailable) {1020return;1021}1022if (!this._inProcessContinueOnComments.has(thread.uniqueOwner)) {1023this._inProcessContinueOnComments.set(thread.uniqueOwner, []);1024}1025this._inProcessContinueOnComments.get(thread.uniqueOwner)?.push(thread);1026await this.commentService.createCommentThreadTemplate(thread.uniqueOwner, thread.uri, thread.range ? Range.lift(thread.range) : undefined);1027}1028}10291030private beginComputeAndHandleEditorChange(): void {1031this.beginCompute().then(() => {1032if (!this._hasRespondedToEditorChange) {1033if (this._commentInfos.some(commentInfo => commentInfo.commentingRanges.ranges.length > 0 || commentInfo.commentingRanges.fileComments)) {1034this._hasRespondedToEditorChange = true;1035const verbose = this.configurationService.getValue(AccessibilityVerbositySettingId.Comments);1036if (verbose) {1037const keybinding = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getAriaLabel();1038if (keybinding) {1039status(nls.localize('hasCommentRangesKb', "Editor has commenting ranges, run the command Open Accessibility Help ({0}), for more information.", keybinding));1040} else {1041status(nls.localize('hasCommentRangesNoKb', "Editor has commenting ranges, run the command Open Accessibility Help, which is currently not triggerable via keybinding, for more information."));1042}1043} else {1044status(nls.localize('hasCommentRanges', "Editor has commenting ranges."));1045}1046}1047}1048});1049}10501051private async openCommentsView(thread: languages.CommentThread) {1052if (thread.comments && (thread.comments.length > 0) && threadHasMeaningfulComments(thread)) {1053const openViewState = this.configurationService.getValue<ICommentsConfiguration>(COMMENTS_SECTION).openView;1054if (openViewState === 'file') {1055return this.viewsService.openView(COMMENTS_VIEW_ID);1056} else if (openViewState === 'firstFile' || (openViewState === 'firstFileUnresolved' && thread.state === languages.CommentThreadState.Unresolved)) {1057const hasShownView = this.viewsService.getViewWithId<CommentsPanel>(COMMENTS_VIEW_ID)?.hasRendered;1058if (!hasShownView) {1059return this.viewsService.openView(COMMENTS_VIEW_ID);1060}1061}1062}1063return undefined;1064}10651066private async displayCommentThread(uniqueOwner: string, thread: languages.CommentThread, shouldReveal: boolean, pendingComment: languages.PendingComment | undefined, pendingEdits: { [key: number]: languages.PendingComment } | undefined): Promise<void> {1067const editor = this.editor?.getModel();1068if (!editor) {1069return;1070}1071if (!this.editor || this.isEditorInlineOriginal(this.editor)) {1072return;1073}10741075let continueOnCommentReply: languages.PendingCommentThread | undefined;1076if (thread.range && !pendingComment) {1077continueOnCommentReply = this.commentService.removeContinueOnComment({ uniqueOwner, uri: editor.uri, range: thread.range, isReply: true });1078}1079const zoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, uniqueOwner, thread, pendingComment ?? continueOnCommentReply?.comment, pendingEdits);1080await zoneWidget.display(thread.range, shouldReveal);1081this._commentWidgets.push(zoneWidget);1082this.openCommentsView(thread);1083}10841085private onEditorMouseDown(e: IEditorMouseEvent): void {1086this.mouseDownInfo = this._activeEditorHasCommentingRange.get() ? parseMouseDownInfoFromEvent(e) : null;1087}10881089private onEditorMouseUp(e: IEditorMouseEvent): void {1090const matchedLineNumber = isMouseUpEventDragFromMouseDown(this.mouseDownInfo, e);1091this.mouseDownInfo = null;10921093if (!this.editor || matchedLineNumber === null || !e.target.element) {1094return;1095}1096const mouseUpIsOnDecorator = (e.target.element.className.indexOf('comment-range-glyph') >= 0);10971098const lineNumber = e.target.position!.lineNumber;1099let range: Range | undefined;1100let selection: Range | null | undefined;1101// Check for drag along gutter decoration1102if ((matchedLineNumber !== lineNumber)) {1103if (matchedLineNumber > lineNumber) {1104selection = new Range(matchedLineNumber, this.editor.getModel()!.getLineLength(matchedLineNumber) + 1, lineNumber, 1);1105} else {1106selection = new Range(matchedLineNumber, 1, lineNumber, this.editor.getModel()!.getLineLength(lineNumber) + 1);1107}1108} else if (mouseUpIsOnDecorator) {1109selection = this.editor.getSelection();1110}11111112// Check for selection at line number.1113if (selection && (selection.startLineNumber <= lineNumber) && (lineNumber <= selection.endLineNumber)) {1114range = selection;1115this.editor.setSelection(new Range(selection.endLineNumber, 1, selection.endLineNumber, 1));1116} else if (mouseUpIsOnDecorator) {1117range = new Range(lineNumber, 1, lineNumber, 1);1118}11191120if (range) {1121this.addOrToggleCommentAtLine(range, e);1122}1123}11241125public getCommentsAtLine(commentRange: Range | undefined): ReviewZoneWidget[] {1126return this._commentWidgets.filter(widget => widget.getGlyphPosition() === (commentRange ? commentRange.endLineNumber : 0));1127}11281129public async addOrToggleCommentAtLine(commentRange: Range | undefined, e: IEditorMouseEvent | undefined): Promise<void> {1130// If an add is already in progress, queue the next add and process it after the current one finishes to1131// prevent empty comment threads from being added to the same line.1132if (!this._addInProgress) {1133this._addInProgress = true;1134// The widget's position is undefined until the widget has been displayed, so rely on the glyph position instead1135const existingCommentsAtLine = this.getCommentsAtLine(commentRange);1136if (existingCommentsAtLine.length) {1137const allExpanded = existingCommentsAtLine.every(widget => widget.expanded);1138existingCommentsAtLine.forEach(allExpanded ? widget => widget.collapse(true) : widget => widget.expand(true));1139this.processNextThreadToAdd();1140return;1141} else {1142this.addCommentAtLine(commentRange, e);1143}1144} else {1145this._emptyThreadsToAddQueue.push([commentRange, e]);1146}1147}11481149private processNextThreadToAdd(): void {1150this._addInProgress = false;1151const info = this._emptyThreadsToAddQueue.shift();1152if (info) {1153this.addOrToggleCommentAtLine(info[0], info[1]);1154}1155}11561157private clipUserRangeToCommentRange(userRange: Range, commentRange: Range): Range {1158if (userRange.startLineNumber < commentRange.startLineNumber) {1159userRange = new Range(commentRange.startLineNumber, commentRange.startColumn, userRange.endLineNumber, userRange.endColumn);1160}1161if (userRange.endLineNumber > commentRange.endLineNumber) {1162userRange = new Range(userRange.startLineNumber, userRange.startColumn, commentRange.endLineNumber, commentRange.endColumn);1163}1164return userRange;1165}11661167public addCommentAtLine(range: Range | undefined, e: IEditorMouseEvent | undefined): Promise<void> {1168const newCommentInfos = this._commentingRangeDecorator.getMatchedCommentAction(range);1169if (!newCommentInfos.length || !this.editor?.hasModel()) {1170this._addInProgress = false;1171if (!newCommentInfos.length) {1172if (range) {1173this.notificationService.error(nls.localize('comments.addCommand.error', "The cursor must be within a commenting range to add a comment."));1174} else {1175this.notificationService.error(nls.localize('comments.addFileCommentCommand.error', "File comments are not allowed on this file."));1176}1177}1178return Promise.resolve();1179}11801181if (newCommentInfos.length > 1) {1182if (e && range) {1183this.contextMenuService.showContextMenu({1184getAnchor: () => e.event,1185getActions: () => this.getContextMenuActions(newCommentInfos, range),1186getActionsContext: () => newCommentInfos.length ? newCommentInfos[0] : undefined,1187onHide: () => { this._addInProgress = false; }1188});11891190return Promise.resolve();1191} else {1192const picks = this.getCommentProvidersQuickPicks(newCommentInfos);1193return this.quickInputService.pick(picks, { placeHolder: nls.localize('pickCommentService', "Select Comment Provider"), matchOnDescription: true }).then(pick => {1194if (!pick) {1195return;1196}11971198const commentInfos = newCommentInfos.filter(info => info.action.ownerId === pick.id);11991200if (commentInfos.length) {1201const { ownerId } = commentInfos[0].action;1202const clippedRange = range && commentInfos[0].range ? this.clipUserRangeToCommentRange(range, commentInfos[0].range) : range;1203this.addCommentAtLine2(clippedRange, ownerId);1204}1205}).then(() => {1206this._addInProgress = false;1207});1208}1209} else {1210const { ownerId } = newCommentInfos[0]!.action;1211const clippedRange = range && newCommentInfos[0].range ? this.clipUserRangeToCommentRange(range, newCommentInfos[0].range) : range;1212this.addCommentAtLine2(clippedRange, ownerId);1213}12141215return Promise.resolve();1216}12171218private getCommentProvidersQuickPicks(commentInfos: MergedCommentRangeActions[]) {1219const picks: QuickPickInput[] = commentInfos.map((commentInfo) => {1220const { ownerId, extensionId, label } = commentInfo.action;12211222return {1223label: label ?? extensionId ?? ownerId,1224id: ownerId1225} satisfies IQuickPickItem;1226});12271228return picks;1229}12301231private getContextMenuActions(commentInfos: MergedCommentRangeActions[], commentRange: Range): IAction[] {1232const actions: IAction[] = [];12331234commentInfos.forEach(commentInfo => {1235const { ownerId, extensionId, label } = commentInfo.action;12361237actions.push(new Action(1238'addCommentThread',1239`${label || extensionId}`,1240undefined,1241true,1242() => {1243const clippedRange = commentInfo.range ? this.clipUserRangeToCommentRange(commentRange, commentInfo.range) : commentRange;1244this.addCommentAtLine2(clippedRange, ownerId);1245return Promise.resolve();1246}1247));1248});1249return actions;1250}12511252public addCommentAtLine2(range: Range | undefined, ownerId: string) {1253if (!this.editor) {1254return;1255}1256this.commentService.createCommentThreadTemplate(ownerId, this.editor.getModel()!.uri, range, this.editor.getId());1257this.processNextThreadToAdd();1258return;1259}12601261private getExistingCommentEditorOptions(editor: ICodeEditor) {1262const lineDecorationsWidth: number = editor.getOption(EditorOption.lineDecorationsWidth);1263let extraEditorClassName: string[] = [];1264const configuredExtraClassName = editor.getRawOptions().extraEditorClassName;1265if (configuredExtraClassName) {1266extraEditorClassName = configuredExtraClassName.split(' ');1267}1268return { lineDecorationsWidth, extraEditorClassName };1269}12701271private getWithoutCommentsEditorOptions(editor: ICodeEditor, extraEditorClassName: string[], startingLineDecorationsWidth: number) {1272let lineDecorationsWidth = startingLineDecorationsWidth;1273const inlineCommentPos = extraEditorClassName.findIndex(name => name === 'inline-comment');1274if (inlineCommentPos >= 0) {1275extraEditorClassName.splice(inlineCommentPos, 1);1276}12771278const options = editor.getOptions();1279if (options.get(EditorOption.folding) && options.get(EditorOption.showFoldingControls) !== 'never') {1280lineDecorationsWidth += 11; // 11 comes from https://github.com/microsoft/vscode/blob/94ee5f58619d59170983f453fe78f156c0cc73a3/src/vs/workbench/contrib/comments/browser/media/review.css#L4851281}1282lineDecorationsWidth -= 24;1283return { extraEditorClassName, lineDecorationsWidth };1284}12851286private getWithCommentsLineDecorationWidth(editor: ICodeEditor, startingLineDecorationsWidth: number) {1287let lineDecorationsWidth = startingLineDecorationsWidth;1288const options = editor.getOptions();1289if (options.get(EditorOption.folding) && options.get(EditorOption.showFoldingControls) !== 'never') {1290lineDecorationsWidth -= 11;1291}1292lineDecorationsWidth += 24;1293this._commentingRangeAmountReserved = lineDecorationsWidth;1294return this._commentingRangeAmountReserved;1295}12961297private getWithCommentsEditorOptions(editor: ICodeEditor, extraEditorClassName: string[], startingLineDecorationsWidth: number) {1298extraEditorClassName.push('inline-comment');1299return { lineDecorationsWidth: this.getWithCommentsLineDecorationWidth(editor, startingLineDecorationsWidth), extraEditorClassName };1300}13011302private updateEditorLayoutOptions(editor: ICodeEditor, extraEditorClassName: string[], lineDecorationsWidth: number) {1303editor.updateOptions({1304extraEditorClassName: extraEditorClassName.join(' '),1305lineDecorationsWidth: lineDecorationsWidth1306});1307}13081309private ensureCommentingRangeReservedAmount(editor: ICodeEditor) {1310const existing = this.getExistingCommentEditorOptions(editor);1311if (existing.lineDecorationsWidth !== this._commentingRangeAmountReserved) {1312editor.updateOptions({1313lineDecorationsWidth: this.getWithCommentsLineDecorationWidth(editor, existing.lineDecorationsWidth)1314});1315}1316}13171318private tryUpdateReservedSpace(uri?: URI) {1319if (!this.editor) {1320return;1321}13221323const hasCommentsOrRangesInInfo = this._commentInfos.some(info => {1324const hasRanges = Boolean(info.commentingRanges && (Array.isArray(info.commentingRanges) ? info.commentingRanges : info.commentingRanges.ranges).length);1325return hasRanges || (info.threads.length > 0);1326});1327uri = uri ?? this.editor.getModel()?.uri;1328const resourceHasCommentingRanges = uri ? this.commentService.resourceHasCommentingRanges(uri) : false;13291330const hasCommentsOrRanges = hasCommentsOrRangesInInfo || resourceHasCommentingRanges;13311332if (hasCommentsOrRanges && this.commentService.isCommentingEnabled) {1333if (!this._commentingRangeSpaceReserved) {1334this._commentingRangeSpaceReserved = true;1335const { lineDecorationsWidth, extraEditorClassName } = this.getExistingCommentEditorOptions(this.editor);1336const newOptions = this.getWithCommentsEditorOptions(this.editor, extraEditorClassName, lineDecorationsWidth);1337this.updateEditorLayoutOptions(this.editor, newOptions.extraEditorClassName, newOptions.lineDecorationsWidth);1338} else {1339this.ensureCommentingRangeReservedAmount(this.editor);1340}1341} else if ((!hasCommentsOrRanges || !this.commentService.isCommentingEnabled) && this._commentingRangeSpaceReserved) {1342this._commentingRangeSpaceReserved = false;1343const { lineDecorationsWidth, extraEditorClassName } = this.getExistingCommentEditorOptions(this.editor);1344const newOptions = this.getWithoutCommentsEditorOptions(this.editor, extraEditorClassName, lineDecorationsWidth);1345this.updateEditorLayoutOptions(this.editor, newOptions.extraEditorClassName, newOptions.lineDecorationsWidth);1346}1347}13481349private async setComments(commentInfos: ICommentInfo[]): Promise<void> {1350if (!this.editor || !this.commentService.isCommentingEnabled) {1351return;1352}13531354this._commentInfos = commentInfos;1355this.tryUpdateReservedSpace();1356// create viewzones1357this.removeCommentWidgetsAndStoreCache();13581359let hasCommentingRanges = false;1360for (const info of this._commentInfos) {1361if (!hasCommentingRanges && (info.commentingRanges.ranges.length > 0 || info.commentingRanges.fileComments)) {1362hasCommentingRanges = true;1363}13641365const providerCacheStore = this._pendingNewCommentCache[info.uniqueOwner];1366const providerEditsCacheStore = this._pendingEditsCache[info.uniqueOwner];1367info.threads = info.threads.filter(thread => !thread.isDisposed);1368for (const thread of info.threads) {1369let pendingComment: languages.PendingComment | undefined = undefined;1370if (providerCacheStore) {1371pendingComment = providerCacheStore[thread.threadId];1372}13731374let pendingEdits: { [key: number]: languages.PendingComment } | undefined = undefined;1375if (providerEditsCacheStore) {1376pendingEdits = providerEditsCacheStore[thread.threadId];1377}13781379await this.displayCommentThread(info.uniqueOwner, thread, false, pendingComment, pendingEdits);1380}1381for (const thread of info.pendingCommentThreads ?? []) {1382this.resumePendingComment(this.editor!.getModel()!.uri, thread);1383}1384}13851386this._commentingRangeDecorator.update(this.editor, this._commentInfos);1387this._commentThreadRangeDecorator.update(this.editor, this._commentInfos);13881389if (hasCommentingRanges) {1390this._activeEditorHasCommentingRange.set(true);1391} else {1392this._activeEditorHasCommentingRange.set(false);1393}1394}13951396public collapseAndFocusRange(threadId: string): void {1397this._commentWidgets?.find(widget => widget.commentThread.threadId === threadId)?.collapseAndFocusRange();1398}13991400private removeCommentWidgetsAndStoreCache() {1401if (this._commentWidgets) {1402this._commentWidgets.forEach(zone => {1403const pendingComments = zone.getPendingComments();1404const pendingNewComment = pendingComments.newComment;1405const providerNewCommentCacheStore = this._pendingNewCommentCache[zone.uniqueOwner];14061407let lastCommentBody;1408if (zone.commentThread.comments && zone.commentThread.comments.length) {1409const lastComment = zone.commentThread.comments[zone.commentThread.comments.length - 1];1410if (typeof lastComment.body === 'string') {1411lastCommentBody = lastComment.body;1412} else {1413lastCommentBody = lastComment.body.value;1414}1415}1416if (pendingNewComment && (pendingNewComment.body !== lastCommentBody)) {1417if (!providerNewCommentCacheStore) {1418this._pendingNewCommentCache[zone.uniqueOwner] = {};1419}14201421this._pendingNewCommentCache[zone.uniqueOwner][zone.commentThread.threadId] = pendingNewComment;1422} else {1423if (providerNewCommentCacheStore) {1424delete providerNewCommentCacheStore[zone.commentThread.threadId];1425}1426}14271428const pendingEdits = pendingComments.edits;1429const providerEditsCacheStore = this._pendingEditsCache[zone.uniqueOwner];1430if (Object.keys(pendingEdits).length > 0) {1431if (!providerEditsCacheStore) {1432this._pendingEditsCache[zone.uniqueOwner] = {};1433}1434this._pendingEditsCache[zone.uniqueOwner][zone.commentThread.threadId] = pendingEdits;1435} else if (providerEditsCacheStore) {1436delete providerEditsCacheStore[zone.commentThread.threadId];1437}14381439zone.dispose();1440});1441}14421443this._commentWidgets = [];1444}1445}144614471448