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