Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/comments/browser/commentsModel.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 { groupBy } from '../../../../base/common/arrays.js';
7
import { URI } from '../../../../base/common/uri.js';
8
import { CommentThread } from '../../../../editor/common/languages.js';
9
import { localize } from '../../../../nls.js';
10
import { ResourceWithCommentThreads, ICommentThreadChangedEvent } from '../common/commentModel.js';
11
import { Disposable } from '../../../../base/common/lifecycle.js';
12
import { isMarkdownString } from '../../../../base/common/htmlContent.js';
13
14
export function threadHasMeaningfulComments(thread: CommentThread): boolean {
15
return !!thread.comments && !!thread.comments.length && thread.comments.some(comment => isMarkdownString(comment.body) ? comment.body.value.length > 0 : comment.body.length > 0);
16
17
}
18
19
export interface ICommentsModel {
20
hasCommentThreads(): boolean;
21
getMessage(): string;
22
readonly resourceCommentThreads: ResourceWithCommentThreads[];
23
readonly commentThreadsMap: Map<string, { resourceWithCommentThreads: ResourceWithCommentThreads[]; ownerLabel?: string }>;
24
}
25
26
export class CommentsModel extends Disposable implements ICommentsModel {
27
readonly _serviceBrand: undefined;
28
private _resourceCommentThreads: ResourceWithCommentThreads[];
29
get resourceCommentThreads(): ResourceWithCommentThreads[] { return this._resourceCommentThreads; }
30
readonly commentThreadsMap: Map<string, { resourceWithCommentThreads: ResourceWithCommentThreads[]; ownerLabel?: string }>;
31
32
constructor(
33
) {
34
super();
35
this._resourceCommentThreads = [];
36
this.commentThreadsMap = new Map<string, { resourceWithCommentThreads: ResourceWithCommentThreads[]; ownerLabel: string }>();
37
}
38
39
private updateResourceCommentThreads() {
40
const includeLabel = this.commentThreadsMap.size > 1;
41
this._resourceCommentThreads = [...this.commentThreadsMap.values()].map(value => {
42
return value.resourceWithCommentThreads.map(resource => {
43
resource.ownerLabel = includeLabel ? value.ownerLabel : undefined;
44
return resource;
45
}).flat();
46
}).flat();
47
}
48
49
public setCommentThreads(uniqueOwner: string, owner: string, ownerLabel: string, commentThreads: CommentThread[]): void {
50
this.commentThreadsMap.set(uniqueOwner, { ownerLabel, resourceWithCommentThreads: this.groupByResource(uniqueOwner, owner, commentThreads) });
51
this.updateResourceCommentThreads();
52
}
53
54
public deleteCommentsByOwner(uniqueOwner?: string): void {
55
if (uniqueOwner) {
56
const existingOwner = this.commentThreadsMap.get(uniqueOwner);
57
this.commentThreadsMap.set(uniqueOwner, { ownerLabel: existingOwner?.ownerLabel, resourceWithCommentThreads: [] });
58
} else {
59
this.commentThreadsMap.clear();
60
}
61
this.updateResourceCommentThreads();
62
}
63
64
public updateCommentThreads(event: ICommentThreadChangedEvent): boolean {
65
const { uniqueOwner, owner, ownerLabel, removed, changed, added } = event;
66
67
const threadsForOwner = this.commentThreadsMap.get(uniqueOwner)?.resourceWithCommentThreads || [];
68
69
removed.forEach(thread => {
70
// Find resource that has the comment thread
71
const matchingResourceIndex = threadsForOwner.findIndex((resourceData) => resourceData.id === thread.resource);
72
const matchingResourceData = matchingResourceIndex >= 0 ? threadsForOwner[matchingResourceIndex] : undefined;
73
74
// Find comment node on resource that is that thread and remove it
75
const index = matchingResourceData?.commentThreads.findIndex((commentThread) => commentThread.threadId === thread.threadId) ?? 0;
76
if (index >= 0) {
77
matchingResourceData?.commentThreads.splice(index, 1);
78
}
79
80
// If the comment thread was the last thread for a resource, remove that resource from the list
81
if (matchingResourceData?.commentThreads.length === 0) {
82
threadsForOwner.splice(matchingResourceIndex, 1);
83
}
84
});
85
86
changed.forEach(thread => {
87
// Find resource that has the comment thread
88
const matchingResourceIndex = threadsForOwner.findIndex((resourceData) => resourceData.id === thread.resource);
89
const matchingResourceData = matchingResourceIndex >= 0 ? threadsForOwner[matchingResourceIndex] : undefined;
90
if (!matchingResourceData) {
91
return;
92
}
93
94
// Find comment node on resource that is that thread and replace it
95
const index = matchingResourceData.commentThreads.findIndex((commentThread) => commentThread.threadId === thread.threadId);
96
if (index >= 0) {
97
matchingResourceData.commentThreads[index] = ResourceWithCommentThreads.createCommentNode(uniqueOwner, owner, URI.parse(matchingResourceData.id), thread);
98
} else if (thread.comments && thread.comments.length) {
99
matchingResourceData.commentThreads.push(ResourceWithCommentThreads.createCommentNode(uniqueOwner, owner, URI.parse(matchingResourceData.id), thread));
100
}
101
});
102
103
added.forEach(thread => {
104
const existingResource = threadsForOwner.filter(resourceWithThreads => resourceWithThreads.resource.toString() === thread.resource);
105
if (existingResource.length) {
106
const resource = existingResource[0];
107
if (thread.comments && thread.comments.length) {
108
resource.commentThreads.push(ResourceWithCommentThreads.createCommentNode(uniqueOwner, owner, resource.resource, thread));
109
}
110
} else {
111
threadsForOwner.push(new ResourceWithCommentThreads(uniqueOwner, owner, URI.parse(thread.resource!), [thread]));
112
}
113
});
114
115
this.commentThreadsMap.set(uniqueOwner, { ownerLabel, resourceWithCommentThreads: threadsForOwner });
116
this.updateResourceCommentThreads();
117
118
return removed.length > 0 || changed.length > 0 || added.length > 0;
119
}
120
121
public hasCommentThreads(): boolean {
122
// There's a resource with at least one thread
123
return !!this._resourceCommentThreads.length && this._resourceCommentThreads.some(resource => {
124
// At least one of the threads in the resource has comments
125
return (resource.commentThreads.length > 0) && resource.commentThreads.some(thread => {
126
// At least one of the comments in the thread is not empty
127
return threadHasMeaningfulComments(thread.thread);
128
});
129
});
130
}
131
132
public getMessage(): string {
133
if (!this._resourceCommentThreads.length) {
134
return localize('noComments', "There are no comments in this workspace yet.");
135
} else {
136
return '';
137
}
138
}
139
140
private groupByResource(uniqueOwner: string, owner: string, commentThreads: CommentThread[]): ResourceWithCommentThreads[] {
141
const resourceCommentThreads: ResourceWithCommentThreads[] = [];
142
const commentThreadsByResource = new Map<string, ResourceWithCommentThreads>();
143
for (const group of groupBy(commentThreads, CommentsModel._compareURIs)) {
144
commentThreadsByResource.set(group[0].resource!, new ResourceWithCommentThreads(uniqueOwner, owner, URI.parse(group[0].resource!), group));
145
}
146
147
commentThreadsByResource.forEach((v, i, m) => {
148
resourceCommentThreads.push(v);
149
});
150
151
return resourceCommentThreads;
152
}
153
154
private static _compareURIs(a: CommentThread, b: CommentThread) {
155
const resourceA = a.resource!.toString();
156
const resourceB = b.resource!.toString();
157
if (resourceA < resourceB) {
158
return -1;
159
} else if (resourceA > resourceB) {
160
return 1;
161
} else {
162
return 0;
163
}
164
}
165
}
166
167