// ***************************************************************************** // Copyright (C) 2020 Red Hat, Inc. and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { Range, Comment, CommentInput, CommentOptions, CommentThread, CommentThreadChangedEvent } from '../../../common/plugin-api-rpc-model'; import { Event, Emitter } from '@theia/core/lib/common/event'; import { CommentThreadCollapsibleState, CommentThreadState } from '../../../plugin/types-impl'; import { CommentProviderFeatures, CommentsExt, CommentsMain, CommentThreadChanges, MAIN_RPC_CONTEXT } from '../../../common/plugin-api-rpc'; import { Disposable } from '@theia/core/lib/common/disposable'; import { CommentsService, CommentInfoMain } from './comments-service'; import { UriComponents } from '../../../common/uri-components'; import { URI } from '@theia/core/shared/vscode-uri'; import { CancellationToken } from '@theia/core/lib/common'; import { RPCProtocol } from '../../../common/rpc-protocol'; import { interfaces } from '@theia/core/shared/inversify'; import { generateUuid } from '@theia/core/lib/common/uuid'; import { CommentsContribution } from './comments-contribution'; import { CommentAuthorInformation } from '@theia/plugin'; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ // some code copied and modified from https://github.com/microsoft/vscode/blob/1.49.3/src/vs/workbench/api/browser/mainThreadComments.ts export class CommentThreadImpl implements CommentThread, Disposable { private _input?: CommentInput; get input(): CommentInput | undefined { return this._input; } set input(value: CommentInput | undefined) { this._input = value; this.onDidChangeInputEmitter.fire(value); } private readonly onDidChangeInputEmitter = new Emitter(); get onDidChangeInput(): Event { return this.onDidChangeInputEmitter.event; } private _label: string | undefined; get label(): string | undefined { return this._label; } set label(label: string | undefined) { this._label = label; this.onDidChangeLabelEmitter.fire(this._label); } private readonly onDidChangeLabelEmitter = new Emitter(); readonly onDidChangeLabel: Event = this.onDidChangeLabelEmitter.event; private _contextValue: string | undefined; get contextValue(): string | undefined { return this._contextValue; } set contextValue(context: string | undefined) { this._contextValue = context; } private _comments: Comment[] | undefined; public get comments(): Comment[] | undefined { return this._comments; } public set comments(newComments: Comment[] | undefined) { this._comments = newComments; this.onDidChangeCommentsEmitter.fire(this._comments); } private readonly onDidChangeCommentsEmitter = new Emitter(); get onDidChangeComments(): Event { return this.onDidChangeCommentsEmitter.event; } set range(range: Range | undefined) { this._range = range; this.onDidChangeRangeEmitter.fire(this._range); } get range(): Range | undefined { return this._range; } private readonly onDidChangeRangeEmitter = new Emitter(); public onDidChangeRange = this.onDidChangeRangeEmitter.event; private _collapsibleState: CommentThreadCollapsibleState | undefined; get collapsibleState(): CommentThreadCollapsibleState | undefined { return this._collapsibleState; } set collapsibleState(newState: CommentThreadCollapsibleState | undefined) { this._collapsibleState = newState; this.onDidChangeCollapsibleStateEmitter.fire(this._collapsibleState); } private readonly onDidChangeCollapsibleStateEmitter = new Emitter(); readonly onDidChangeCollapsibleState = this.onDidChangeCollapsibleStateEmitter.event; private _state: CommentThreadState | undefined; get state(): CommentThreadState | undefined { return this._state; } set state(newState: CommentThreadState | undefined) { if (this._state !== newState) { this._state = newState; this.onDidChangeStateEmitter.fire(this._state); } } private readonly onDidChangeStateEmitter = new Emitter(); readonly onDidChangeState = this.onDidChangeStateEmitter.event; private readonly onDidChangeCanReplyEmitter = new Emitter(); readonly onDidChangeCanReply = this.onDidChangeCanReplyEmitter.event; private _isDisposed: boolean; get isDisposed(): boolean { return this._isDisposed; } private _canReply: boolean | CommentAuthorInformation = true; get canReply(): boolean | CommentAuthorInformation { return this._canReply; } set canReply(canReply: boolean | CommentAuthorInformation) { this._canReply = canReply; this.onDidChangeCanReplyEmitter.fire(this._canReply); } constructor( public commentThreadHandle: number, public controllerHandle: number, public extensionId: string, public threadId: string, public resource: string, private _range: Range | undefined ) { this._isDisposed = false; } batchUpdate(changes: CommentThreadChanges): void { const modified = (value: keyof CommentThreadChanges): boolean => Object.prototype.hasOwnProperty.call(changes, value); if (modified('range')) { this._range = changes.range; } if (modified('label')) { this._label = changes.label; } if (modified('contextValue')) { this._contextValue = changes.contextValue; } if (modified('comments')) { this._comments = changes.comments; } if (modified('collapseState')) { this._collapsibleState = changes.collapseState; } if (modified('state')) { this._state = changes.state; } if (modified('canReply')) { this._canReply = changes.canReply!; } } dispose(): void { this._isDisposed = true; this.onDidChangeCollapsibleStateEmitter.dispose(); this.onDidChangeStateEmitter.dispose(); this.onDidChangeCommentsEmitter.dispose(); this.onDidChangeInputEmitter.dispose(); this.onDidChangeLabelEmitter.dispose(); this.onDidChangeRangeEmitter.dispose(); this.onDidChangeCanReplyEmitter.dispose(); } } export class CommentController { get handle(): number { return this._handle; } get id(): string { return this._id; } get contextValue(): string { return this._id; } get proxy(): CommentsExt { return this._proxy; } get label(): string { return this._label; } get options(): CommentOptions | undefined { return this._features.options; } private readonly threads: Map = new Map(); public activeCommentThread?: CommentThread; get features(): CommentProviderFeatures { return this._features; } constructor( private readonly _proxy: CommentsExt, private readonly _commentService: CommentsService, private readonly _handle: number, private readonly _uniqueId: string, private readonly _id: string, private readonly _label: string, private _features: CommentProviderFeatures ) { } updateFeatures(features: CommentProviderFeatures): void { this._features = features; } createCommentThread(extensionId: string, commentThreadHandle: number, threadId: string, resource: UriComponents, range: Range | undefined, ): CommentThread { const thread = new CommentThreadImpl( commentThreadHandle, this.handle, extensionId, threadId, URI.revive(resource).toString(), range ); this.threads.set(commentThreadHandle, thread); this._commentService.updateComments(this._uniqueId, { added: [thread], removed: [], changed: [] }); return thread; } updateCommentThread(commentThreadHandle: number, threadId: string, resource: UriComponents, changes: CommentThreadChanges): void { const thread = this.getKnownThread(commentThreadHandle); thread.batchUpdate(changes); this._commentService.updateComments(this._uniqueId, { added: [], removed: [], changed: [thread] }); } deleteCommentThread(commentThreadHandle: number): void { const thread = this.getKnownThread(commentThreadHandle); this.threads.delete(commentThreadHandle); this._commentService.updateComments(this._uniqueId, { added: [], removed: [thread], changed: [] }); thread.dispose(); } deleteCommentThreadMain(commentThreadId: string): void { this.threads.forEach(thread => { if (thread.threadId === commentThreadId) { this._proxy.$deleteCommentThread(this._handle, thread.commentThreadHandle); } }); } updateInput(input: string): void { const thread = this.activeCommentThread; if (thread && thread.input) { const commentInput = thread.input; commentInput.value = input; thread.input = commentInput; } } private getKnownThread(commentThreadHandle: number): CommentThreadImpl { const thread = this.threads.get(commentThreadHandle); if (!thread) { throw new Error('unknown thread'); } return thread; } async getDocumentComments(resource: URI, token: CancellationToken): Promise { const ret: CommentThread[] = []; for (const thread of [...this.threads.keys()]) { const commentThread = this.threads.get(thread)!; if (commentThread.resource === resource.toString()) { ret.push(commentThread); } } const commentingRanges = await this._proxy.$provideCommentingRanges(this.handle, resource, token); return { owner: this._uniqueId, label: this.label, threads: ret, commentingRanges: { resource: resource, ranges: commentingRanges?.ranges || [], fileComments: !!commentingRanges?.fileComments } }; } async getCommentingRanges(resource: URI, token: CancellationToken): Promise<{ ranges: Range[]; fileComments: boolean } | undefined> { const commentingRanges = await this._proxy.$provideCommentingRanges(this.handle, resource, token); return commentingRanges; } getAllComments(): CommentThread[] { const ret: CommentThread[] = []; for (const thread of [...this.threads.keys()]) { ret.push(this.threads.get(thread)!); } return ret; } createCommentThreadTemplate(resource: UriComponents, range: Range): void { this._proxy.$createCommentThreadTemplate(this.handle, resource, range); } async updateCommentThreadTemplate(threadHandle: number, range: Range): Promise { await this._proxy.$updateCommentThreadTemplate(this.handle, threadHandle, range); } } export class CommentsMainImp implements CommentsMain { private readonly proxy: CommentsExt; private documentProviders = new Map(); private workspaceProviders = new Map(); private handlers = new Map(); private commentControllers = new Map(); private activeCommentThread?: CommentThread; private readonly commentService: CommentsService; constructor(rpc: RPCProtocol, container: interfaces.Container) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.COMMENTS_EXT); container.get(CommentsContribution); this.commentService = container.get(CommentsService); this.commentService.onDidChangeActiveCommentThread(async thread => { const handle = (thread as CommentThread).controllerHandle; const controller = this.commentControllers.get(handle); if (!controller) { return; } this.activeCommentThread = thread as CommentThread; controller.activeCommentThread = this.activeCommentThread; }); } $registerCommentController(handle: number, id: string, label: string): void { const providerId = generateUuid(); this.handlers.set(handle, providerId); const provider = new CommentController(this.proxy, this.commentService, handle, providerId, id, label, {}); this.commentService.registerCommentController(providerId, provider); this.commentControllers.set(handle, provider); this.commentService.setWorkspaceComments(String(handle), []); } $unregisterCommentController(handle: number): void { const providerId = this.handlers.get(handle); if (typeof providerId !== 'string') { throw new Error('unknown handler'); } this.commentService.unregisterCommentController(providerId); this.handlers.delete(handle); this.commentControllers.delete(handle); } $updateCommentControllerFeatures(handle: number, features: CommentProviderFeatures): void { const provider = this.commentControllers.get(handle); if (!provider) { return undefined; } provider.updateFeatures(features); } $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: Range | undefined, extensionId: string ): CommentThread | undefined { const provider = this.commentControllers.get(handle); if (!provider) { return undefined; } return provider.createCommentThread(extensionId, commentThreadHandle, threadId, resource, range); } $updateCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, changes: CommentThreadChanges): void { const provider = this.commentControllers.get(handle); if (!provider) { return undefined; } return provider.updateCommentThread(commentThreadHandle, threadId, resource, changes); } $deleteCommentThread(handle: number, commentThreadHandle: number): void { const provider = this.commentControllers.get(handle); if (!provider) { return; } return provider.deleteCommentThread(commentThreadHandle); } private getHandler(handle: number): string { if (!this.handlers.has(handle)) { throw new Error('Unknown handler'); } return this.handlers.get(handle)!; } $onDidCommentThreadsChange(handle: number, event: CommentThreadChangedEvent): void { const providerId = this.getHandler(handle); this.commentService.updateComments(providerId, event); } dispose(): void { this.workspaceProviders.forEach(value => value.dispose()); this.workspaceProviders.clear(); this.documentProviders.forEach(value => value.dispose()); this.documentProviders.clear(); } }