Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts
5221 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as dom from '../../../../base/browser/dom.js';
7
import * as nls from '../../../../nls.js';
8
import { Disposable, DisposableMap, DisposableStore } from '../../../../base/common/lifecycle.js';
9
import * as languages from '../../../../editor/common/languages.js';
10
import { Emitter } from '../../../../base/common/event.js';
11
import { ICommentService } from './commentService.js';
12
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
13
import { KeyCode } from '../../../../base/common/keyCodes.js';
14
import { CommentNode } from './commentNode.js';
15
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
16
import { URI } from '../../../../base/common/uri.js';
17
import { ICommentThreadWidget } from '../common/commentThreadWidget.js';
18
import { IMarkdownRendererExtraOptions } from '../../../../platform/markdown/browser/markdownRenderer.js';
19
import { ICellRange } from '../../notebook/common/notebookRange.js';
20
import { IRange } from '../../../../editor/common/core/range.js';
21
import { LayoutableEditor } from './simpleCommentEditor.js';
22
23
export class CommentThreadBody<T extends IRange | ICellRange = IRange> extends Disposable {
24
private _commentsElement!: HTMLElement;
25
private _commentElements: CommentNode<T>[] = [];
26
private _resizeObserver: MutationObserver | null = null;
27
private _focusedComment: number | undefined = undefined;
28
private _onDidResize = new Emitter<dom.Dimension>();
29
onDidResize = this._onDidResize.event;
30
31
private _commentDisposable = new DisposableMap<CommentNode<T>, DisposableStore>();
32
33
get length() {
34
return this._commentThread.comments ? this._commentThread.comments.length : 0;
35
}
36
37
get activeComment() {
38
return this._commentElements.filter(node => node.isEditing)[0];
39
}
40
41
constructor(
42
private readonly _parentEditor: LayoutableEditor,
43
readonly owner: string,
44
readonly parentResourceUri: URI,
45
readonly container: HTMLElement,
46
private _markdownRendererOptions: IMarkdownRendererExtraOptions,
47
private _commentThread: languages.CommentThread<T>,
48
private _pendingEdits: { [key: number]: languages.PendingComment } | undefined,
49
private _scopedInstatiationService: IInstantiationService,
50
private _parentCommentThreadWidget: ICommentThreadWidget,
51
@ICommentService private readonly commentService: ICommentService,
52
) {
53
super();
54
55
this._register(dom.addDisposableListener(container, dom.EventType.FOCUS_IN, e => {
56
// TODO @rebornix, limit T to IRange | ICellRange
57
this.commentService.setActiveEditingCommentThread(this._commentThread);
58
}));
59
}
60
61
focus(commentUniqueId?: number) {
62
if (commentUniqueId !== undefined) {
63
const comment = this._commentElements.find(commentNode => commentNode.comment.uniqueIdInThread === commentUniqueId);
64
if (comment) {
65
comment.focus();
66
return;
67
}
68
}
69
this._commentsElement.focus();
70
}
71
72
hasCommentsInEditMode() {
73
return this._commentElements.some(commentNode => commentNode.isEditing);
74
}
75
76
ensureFocusIntoNewEditingComment() {
77
if (this._commentElements.length === 1 && this._commentElements[0].isEditing) {
78
this._commentElements[0].setFocus(true);
79
}
80
}
81
82
async display() {
83
this._commentsElement = dom.append(this.container, dom.$('div.comments-container'));
84
this._commentsElement.setAttribute('role', 'presentation');
85
this._commentsElement.tabIndex = 0;
86
this._updateAriaLabel();
87
88
this._register(dom.addDisposableListener(this._commentsElement, dom.EventType.KEY_DOWN, (e) => {
89
const event = new StandardKeyboardEvent(e as KeyboardEvent);
90
if ((event.equals(KeyCode.UpArrow) || event.equals(KeyCode.DownArrow)) && (!this._focusedComment || !this._commentElements[this._focusedComment].isEditing)) {
91
const moveFocusWithinBounds = (change: number): number => {
92
if (this._focusedComment === undefined && change >= 0) { return 0; }
93
if (this._focusedComment === undefined && change < 0) { return this._commentElements.length - 1; }
94
const newIndex = this._focusedComment! + change;
95
return Math.min(Math.max(0, newIndex), this._commentElements.length - 1);
96
};
97
98
this._setFocusedComment(event.equals(KeyCode.UpArrow) ? moveFocusWithinBounds(-1) : moveFocusWithinBounds(1));
99
}
100
}));
101
102
this._commentDisposable.clearAndDisposeAll();
103
this._commentElements = [];
104
if (this._commentThread.comments) {
105
for (const comment of this._commentThread.comments) {
106
const newCommentNode = this.createNewCommentNode(comment);
107
108
this._commentElements.push(newCommentNode);
109
this._commentsElement.appendChild(newCommentNode.domNode);
110
if (comment.mode === languages.CommentMode.Editing) {
111
await newCommentNode.switchToEditMode();
112
}
113
}
114
}
115
116
this._resizeObserver = new MutationObserver(this._refresh.bind(this));
117
118
this._resizeObserver.observe(this.container, {
119
attributes: true,
120
childList: true,
121
characterData: true,
122
subtree: true
123
});
124
}
125
126
private _containerClientArea: dom.Dimension | undefined = undefined;
127
private _refresh() {
128
const dimensions = dom.getClientArea(this.container);
129
if ((dimensions.height === 0 && dimensions.width === 0) || (dom.Dimension.equals(this._containerClientArea, dimensions))) {
130
return;
131
}
132
this._containerClientArea = dimensions;
133
this._onDidResize.fire(dimensions);
134
}
135
136
getDimensions() {
137
return dom.getClientArea(this.container);
138
}
139
140
layout(widthInPixel?: number) {
141
this._commentElements.forEach(element => {
142
element.layout(widthInPixel);
143
});
144
}
145
146
getPendingEdits(): { [key: number]: languages.PendingComment } {
147
const pendingEdits: { [key: number]: languages.PendingComment } = {};
148
this._commentElements.forEach(element => {
149
if (element.isEditing) {
150
const pendingEdit = element.getPendingEdit();
151
if (pendingEdit) {
152
pendingEdits[element.comment.uniqueIdInThread] = pendingEdit;
153
}
154
}
155
});
156
157
return pendingEdits;
158
}
159
160
getCommentCoords(commentUniqueId: number): { thread: dom.IDomNodePagePosition; comment: dom.IDomNodePagePosition } | undefined {
161
const matchedNode = this._commentElements.filter(commentNode => commentNode.comment.uniqueIdInThread === commentUniqueId);
162
if (matchedNode && matchedNode.length) {
163
const commentThreadCoords = dom.getDomNodePagePosition(this._commentElements[0].domNode);
164
const commentCoords = dom.getDomNodePagePosition(matchedNode[0].domNode);
165
return {
166
thread: commentThreadCoords,
167
comment: commentCoords
168
};
169
}
170
171
return;
172
}
173
174
async updateCommentThread(commentThread: languages.CommentThread<T>, preserveFocus: boolean) {
175
const oldCommentsLen = this._commentElements.length;
176
const newCommentsLen = commentThread.comments ? commentThread.comments.length : 0;
177
178
const commentElementsToDel: CommentNode<T>[] = [];
179
const commentElementsToDelIndex: number[] = [];
180
for (let i = 0; i < oldCommentsLen; i++) {
181
const comment = this._commentElements[i].comment;
182
const newComment = commentThread.comments ? commentThread.comments.filter(c => c.uniqueIdInThread === comment.uniqueIdInThread) : [];
183
184
if (newComment.length) {
185
this._commentElements[i].update(newComment[0]);
186
} else {
187
commentElementsToDelIndex.push(i);
188
commentElementsToDel.push(this._commentElements[i]);
189
}
190
}
191
192
// del removed elements
193
for (let i = commentElementsToDel.length - 1; i >= 0; i--) {
194
const commentToDelete = commentElementsToDel[i];
195
this._commentDisposable.deleteAndDispose(commentToDelete);
196
197
this._commentElements.splice(commentElementsToDelIndex[i], 1);
198
commentToDelete.domNode.remove();
199
}
200
201
202
let lastCommentElement: HTMLElement | null = null;
203
const newCommentNodeList: CommentNode<T>[] = [];
204
const newCommentsInEditMode: CommentNode<T>[] = [];
205
const startEditing: Promise<void>[] = [];
206
207
for (let i = newCommentsLen - 1; i >= 0; i--) {
208
const currentComment = commentThread.comments![i];
209
const oldCommentNode = this._commentElements.filter(commentNode => commentNode.comment.uniqueIdInThread === currentComment.uniqueIdInThread);
210
if (oldCommentNode.length) {
211
lastCommentElement = oldCommentNode[0].domNode;
212
newCommentNodeList.unshift(oldCommentNode[0]);
213
} else {
214
const newElement = this.createNewCommentNode(currentComment);
215
216
newCommentNodeList.unshift(newElement);
217
if (lastCommentElement) {
218
this._commentsElement.insertBefore(newElement.domNode, lastCommentElement);
219
lastCommentElement = newElement.domNode;
220
} else {
221
this._commentsElement.appendChild(newElement.domNode);
222
lastCommentElement = newElement.domNode;
223
}
224
225
if (currentComment.mode === languages.CommentMode.Editing) {
226
startEditing.push(newElement.switchToEditMode());
227
newCommentsInEditMode.push(newElement);
228
}
229
}
230
}
231
232
this._commentThread = commentThread;
233
this._commentElements = newCommentNodeList;
234
// Start editing *after* updating the thread and elements to avoid a sequencing issue https://github.com/microsoft/vscode/issues/239191
235
await Promise.all(startEditing);
236
237
if (newCommentsInEditMode.length) {
238
const lastIndex = this._commentElements.indexOf(newCommentsInEditMode[newCommentsInEditMode.length - 1]);
239
this._focusedComment = lastIndex;
240
}
241
242
this._updateAriaLabel();
243
if (!preserveFocus) {
244
this._setFocusedComment(this._focusedComment);
245
}
246
}
247
248
private _updateAriaLabel() {
249
if (this._commentThread.isDocumentCommentThread()) {
250
if (this._commentThread.range) {
251
this._commentsElement.ariaLabel = nls.localize('commentThreadAria.withRange', "Comment thread with {0} comments on lines {1} through {2}. {3}.",
252
this._commentThread.comments?.length, this._commentThread.range.startLineNumber, this._commentThread.range.endLineNumber,
253
this._commentThread.label);
254
} else {
255
this._commentsElement.ariaLabel = nls.localize('commentThreadAria.document', "Comment thread with {0} comments on the entire document. {1}.",
256
this._commentThread.comments?.length, this._commentThread.label);
257
}
258
} else {
259
this._commentsElement.ariaLabel = nls.localize('commentThreadAria', "Comment thread with {0} comments. {1}.",
260
this._commentThread.comments?.length, this._commentThread.label);
261
}
262
}
263
264
private _setFocusedComment(value: number | undefined) {
265
if (this._focusedComment !== undefined) {
266
this._commentElements[this._focusedComment]?.setFocus(false);
267
}
268
269
if (this._commentElements.length === 0 || value === undefined) {
270
this._focusedComment = undefined;
271
} else {
272
this._focusedComment = Math.min(value, this._commentElements.length - 1);
273
this._commentElements[this._focusedComment].setFocus(true);
274
}
275
}
276
277
private createNewCommentNode(comment: languages.Comment): CommentNode<T> {
278
const newCommentNode = this._scopedInstatiationService.createInstance(CommentNode,
279
this._parentEditor,
280
this._commentThread,
281
comment,
282
this._pendingEdits ? this._pendingEdits[comment.uniqueIdInThread] : undefined,
283
this.owner,
284
this.parentResourceUri,
285
this._parentCommentThreadWidget,
286
this._markdownRendererOptions) as unknown as CommentNode<T>;
287
288
const disposables: DisposableStore = new DisposableStore();
289
disposables.add(newCommentNode.onDidClick(clickedNode =>
290
this._setFocusedComment(this._commentElements.findIndex(commentNode => commentNode.comment.uniqueIdInThread === clickedNode.comment.uniqueIdInThread))
291
));
292
disposables.add(newCommentNode);
293
this._commentDisposable.set(newCommentNode, disposables);
294
295
return newCommentNode;
296
}
297
298
public override dispose(): void {
299
super.dispose();
300
301
if (this._resizeObserver) {
302
this._resizeObserver.disconnect();
303
this._resizeObserver = null;
304
}
305
306
this._commentDisposable.dispose();
307
}
308
}
309
310