Path: blob/main/src/vs/workbench/contrib/comments/browser/commentsModel.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 { groupBy } from '../../../../base/common/arrays.js';6import { URI } from '../../../../base/common/uri.js';7import { CommentThread } from '../../../../editor/common/languages.js';8import { localize } from '../../../../nls.js';9import { ResourceWithCommentThreads, ICommentThreadChangedEvent } from '../common/commentModel.js';10import { Disposable } from '../../../../base/common/lifecycle.js';11import { isMarkdownString } from '../../../../base/common/htmlContent.js';1213export function threadHasMeaningfulComments(thread: CommentThread): boolean {14return !!thread.comments && !!thread.comments.length && thread.comments.some(comment => isMarkdownString(comment.body) ? comment.body.value.length > 0 : comment.body.length > 0);1516}1718export interface ICommentsModel {19hasCommentThreads(): boolean;20getMessage(): string;21readonly resourceCommentThreads: ResourceWithCommentThreads[];22readonly commentThreadsMap: Map<string, { resourceWithCommentThreads: ResourceWithCommentThreads[]; ownerLabel?: string }>;23}2425export class CommentsModel extends Disposable implements ICommentsModel {26readonly _serviceBrand: undefined;27private _resourceCommentThreads: ResourceWithCommentThreads[];28get resourceCommentThreads(): ResourceWithCommentThreads[] { return this._resourceCommentThreads; }29readonly commentThreadsMap: Map<string, { resourceWithCommentThreads: ResourceWithCommentThreads[]; ownerLabel?: string }>;3031constructor(32) {33super();34this._resourceCommentThreads = [];35this.commentThreadsMap = new Map<string, { resourceWithCommentThreads: ResourceWithCommentThreads[]; ownerLabel: string }>();36}3738private updateResourceCommentThreads() {39const includeLabel = this.commentThreadsMap.size > 1;40this._resourceCommentThreads = [...this.commentThreadsMap.values()].map(value => {41return value.resourceWithCommentThreads.map(resource => {42resource.ownerLabel = includeLabel ? value.ownerLabel : undefined;43return resource;44}).flat();45}).flat();46}4748public setCommentThreads(uniqueOwner: string, owner: string, ownerLabel: string, commentThreads: CommentThread[]): void {49this.commentThreadsMap.set(uniqueOwner, { ownerLabel, resourceWithCommentThreads: this.groupByResource(uniqueOwner, owner, commentThreads) });50this.updateResourceCommentThreads();51}5253public deleteCommentsByOwner(uniqueOwner?: string): void {54if (uniqueOwner) {55const existingOwner = this.commentThreadsMap.get(uniqueOwner);56this.commentThreadsMap.set(uniqueOwner, { ownerLabel: existingOwner?.ownerLabel, resourceWithCommentThreads: [] });57} else {58this.commentThreadsMap.clear();59}60this.updateResourceCommentThreads();61}6263public updateCommentThreads(event: ICommentThreadChangedEvent): boolean {64const { uniqueOwner, owner, ownerLabel, removed, changed, added } = event;6566const threadsForOwner = this.commentThreadsMap.get(uniqueOwner)?.resourceWithCommentThreads || [];6768removed.forEach(thread => {69// Find resource that has the comment thread70const matchingResourceIndex = threadsForOwner.findIndex((resourceData) => resourceData.id === thread.resource);71const matchingResourceData = matchingResourceIndex >= 0 ? threadsForOwner[matchingResourceIndex] : undefined;7273// Find comment node on resource that is that thread and remove it74const index = matchingResourceData?.commentThreads.findIndex((commentThread) => commentThread.threadId === thread.threadId) ?? 0;75if (index >= 0) {76matchingResourceData?.commentThreads.splice(index, 1);77}7879// If the comment thread was the last thread for a resource, remove that resource from the list80if (matchingResourceData?.commentThreads.length === 0) {81threadsForOwner.splice(matchingResourceIndex, 1);82}83});8485changed.forEach(thread => {86// Find resource that has the comment thread87const matchingResourceIndex = threadsForOwner.findIndex((resourceData) => resourceData.id === thread.resource);88const matchingResourceData = matchingResourceIndex >= 0 ? threadsForOwner[matchingResourceIndex] : undefined;89if (!matchingResourceData) {90return;91}9293// Find comment node on resource that is that thread and replace it94const index = matchingResourceData.commentThreads.findIndex((commentThread) => commentThread.threadId === thread.threadId);95if (index >= 0) {96matchingResourceData.commentThreads[index] = ResourceWithCommentThreads.createCommentNode(uniqueOwner, owner, URI.parse(matchingResourceData.id), thread);97} else if (thread.comments && thread.comments.length) {98matchingResourceData.commentThreads.push(ResourceWithCommentThreads.createCommentNode(uniqueOwner, owner, URI.parse(matchingResourceData.id), thread));99}100});101102added.forEach(thread => {103const existingResource = threadsForOwner.filter(resourceWithThreads => resourceWithThreads.resource.toString() === thread.resource);104if (existingResource.length) {105const resource = existingResource[0];106if (thread.comments && thread.comments.length) {107resource.commentThreads.push(ResourceWithCommentThreads.createCommentNode(uniqueOwner, owner, resource.resource, thread));108}109} else {110threadsForOwner.push(new ResourceWithCommentThreads(uniqueOwner, owner, URI.parse(thread.resource!), [thread]));111}112});113114this.commentThreadsMap.set(uniqueOwner, { ownerLabel, resourceWithCommentThreads: threadsForOwner });115this.updateResourceCommentThreads();116117return removed.length > 0 || changed.length > 0 || added.length > 0;118}119120public hasCommentThreads(): boolean {121// There's a resource with at least one thread122return !!this._resourceCommentThreads.length && this._resourceCommentThreads.some(resource => {123// At least one of the threads in the resource has comments124return (resource.commentThreads.length > 0) && resource.commentThreads.some(thread => {125// At least one of the comments in the thread is not empty126return threadHasMeaningfulComments(thread.thread);127});128});129}130131public getMessage(): string {132if (!this._resourceCommentThreads.length) {133return localize('noComments', "There are no comments in this workspace yet.");134} else {135return '';136}137}138139private groupByResource(uniqueOwner: string, owner: string, commentThreads: CommentThread[]): ResourceWithCommentThreads[] {140const resourceCommentThreads: ResourceWithCommentThreads[] = [];141const commentThreadsByResource = new Map<string, ResourceWithCommentThreads>();142for (const group of groupBy(commentThreads, CommentsModel._compareURIs)) {143commentThreadsByResource.set(group[0].resource!, new ResourceWithCommentThreads(uniqueOwner, owner, URI.parse(group[0].resource!), group));144}145146commentThreadsByResource.forEach((v, i, m) => {147resourceCommentThreads.push(v);148});149150return resourceCommentThreads;151}152153private static _compareURIs(a: CommentThread, b: CommentThread) {154const resourceA = a.resource!.toString();155const resourceB = b.resource!.toString();156if (resourceA < resourceB) {157return -1;158} else if (resourceA > resourceB) {159return 1;160} else {161return 0;162}163}164}165166167