mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-27 20:13:32 +01:00
Emitters should generally not be reassigned as their corresponding event would also be lost. Marking these as readonly just to enforce this
538 lines
16 KiB
TypeScript
538 lines
16 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { asPromise } from 'vs/base/common/async';
|
|
import { CancellationToken } from 'vs/base/common/cancellation';
|
|
import { debounce } from 'vs/base/common/decorators';
|
|
import { Emitter } from 'vs/base/common/event';
|
|
import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
|
|
import { URI, UriComponents } from 'vs/base/common/uri';
|
|
import { IRange } from 'vs/editor/common/core/range';
|
|
import * as modes from 'vs/editor/common/modes';
|
|
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
|
import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments';
|
|
import * as extHostTypeConverter from 'vs/workbench/api/common/extHostTypeConverters';
|
|
import * as types from 'vs/workbench/api/common/extHostTypes';
|
|
import * as vscode from 'vscode';
|
|
import { ExtHostCommentsShape, IMainContext, MainContext, MainThreadCommentsShape } from './extHost.protocol';
|
|
import { ExtHostCommands } from './extHostCommands';
|
|
|
|
type ProviderHandle = number;
|
|
|
|
export class ExtHostComments implements ExtHostCommentsShape, IDisposable {
|
|
|
|
private static handlePool = 0;
|
|
|
|
private _proxy: MainThreadCommentsShape;
|
|
|
|
private _commentControllers: Map<ProviderHandle, ExtHostCommentController> = new Map<ProviderHandle, ExtHostCommentController>();
|
|
|
|
private _commentControllersByExtension: Map<string, ExtHostCommentController[]> = new Map<string, ExtHostCommentController[]>();
|
|
|
|
|
|
constructor(
|
|
mainContext: IMainContext,
|
|
commands: ExtHostCommands,
|
|
private readonly _documents: ExtHostDocuments,
|
|
) {
|
|
this._proxy = mainContext.getProxy(MainContext.MainThreadComments);
|
|
|
|
commands.registerArgumentProcessor({
|
|
processArgument: arg => {
|
|
if (arg && arg.$mid === 6) {
|
|
const commentController = this._commentControllers.get(arg.handle);
|
|
|
|
if (!commentController) {
|
|
return arg;
|
|
}
|
|
|
|
return commentController;
|
|
} else if (arg && arg.$mid === 7) {
|
|
const commentController = this._commentControllers.get(arg.commentControlHandle);
|
|
|
|
if (!commentController) {
|
|
return arg;
|
|
}
|
|
|
|
const commentThread = commentController.getCommentThread(arg.commentThreadHandle);
|
|
|
|
if (!commentThread) {
|
|
return arg;
|
|
}
|
|
|
|
return commentThread;
|
|
} else if (arg && arg.$mid === 8) {
|
|
const commentController = this._commentControllers.get(arg.thread.commentControlHandle);
|
|
|
|
if (!commentController) {
|
|
return arg;
|
|
}
|
|
|
|
const commentThread = commentController.getCommentThread(arg.thread.commentThreadHandle);
|
|
|
|
if (!commentThread) {
|
|
return arg;
|
|
}
|
|
|
|
return {
|
|
thread: commentThread,
|
|
text: arg.text
|
|
};
|
|
} else if (arg && arg.$mid === 9) {
|
|
const commentController = this._commentControllers.get(arg.thread.commentControlHandle);
|
|
|
|
if (!commentController) {
|
|
return arg;
|
|
}
|
|
|
|
const commentThread = commentController.getCommentThread(arg.thread.commentThreadHandle);
|
|
|
|
if (!commentThread) {
|
|
return arg;
|
|
}
|
|
|
|
let commentUniqueId = arg.commentUniqueId;
|
|
|
|
let comment = commentThread.getCommentByUniqueId(commentUniqueId);
|
|
|
|
if (!comment) {
|
|
return arg;
|
|
}
|
|
|
|
return comment;
|
|
|
|
} else if (arg && arg.$mid === 10) {
|
|
const commentController = this._commentControllers.get(arg.thread.commentControlHandle);
|
|
|
|
if (!commentController) {
|
|
return arg;
|
|
}
|
|
|
|
const commentThread = commentController.getCommentThread(arg.thread.commentThreadHandle);
|
|
|
|
if (!commentThread) {
|
|
return arg;
|
|
}
|
|
|
|
let body = arg.text;
|
|
let commentUniqueId = arg.commentUniqueId;
|
|
|
|
let comment = commentThread.getCommentByUniqueId(commentUniqueId);
|
|
|
|
if (!comment) {
|
|
return arg;
|
|
}
|
|
|
|
comment.body = body;
|
|
return comment;
|
|
}
|
|
|
|
return arg;
|
|
}
|
|
});
|
|
}
|
|
|
|
createCommentController(extension: IExtensionDescription, id: string, label: string): vscode.CommentController {
|
|
const handle = ExtHostComments.handlePool++;
|
|
const commentController = new ExtHostCommentController(extension, handle, this._proxy, id, label);
|
|
this._commentControllers.set(commentController.handle, commentController);
|
|
|
|
const commentControllers = this._commentControllersByExtension.get(ExtensionIdentifier.toKey(extension.identifier)) || [];
|
|
commentControllers.push(commentController);
|
|
this._commentControllersByExtension.set(ExtensionIdentifier.toKey(extension.identifier), commentControllers);
|
|
|
|
return commentController;
|
|
}
|
|
|
|
$createCommentThreadTemplate(commentControllerHandle: number, uriComponents: UriComponents, range: IRange): void {
|
|
const commentController = this._commentControllers.get(commentControllerHandle);
|
|
|
|
if (!commentController) {
|
|
return;
|
|
}
|
|
|
|
commentController.$createCommentThreadTemplate(uriComponents, range);
|
|
}
|
|
|
|
async $updateCommentThreadTemplate(commentControllerHandle: number, threadHandle: number, range: IRange) {
|
|
const commentController = this._commentControllers.get(commentControllerHandle);
|
|
|
|
if (!commentController) {
|
|
return;
|
|
}
|
|
|
|
commentController.$updateCommentThreadTemplate(threadHandle, range);
|
|
}
|
|
|
|
$deleteCommentThread(commentControllerHandle: number, commentThreadHandle: number) {
|
|
const commentController = this._commentControllers.get(commentControllerHandle);
|
|
|
|
if (commentController) {
|
|
commentController.$deleteCommentThread(commentThreadHandle);
|
|
}
|
|
}
|
|
|
|
$provideCommentingRanges(commentControllerHandle: number, uriComponents: UriComponents, token: CancellationToken): Promise<IRange[] | undefined> {
|
|
const commentController = this._commentControllers.get(commentControllerHandle);
|
|
|
|
if (!commentController || !commentController.commentingRangeProvider) {
|
|
return Promise.resolve(undefined);
|
|
}
|
|
|
|
const document = this._documents.getDocument(URI.revive(uriComponents));
|
|
return asPromise(() => {
|
|
return commentController.commentingRangeProvider!.provideCommentingRanges(document, token);
|
|
}).then(ranges => ranges ? ranges.map(x => extHostTypeConverter.Range.from(x)) : undefined);
|
|
}
|
|
|
|
$toggleReaction(commentControllerHandle: number, threadHandle: number, uri: UriComponents, comment: modes.Comment, reaction: modes.CommentReaction): Promise<void> {
|
|
const commentController = this._commentControllers.get(commentControllerHandle);
|
|
|
|
if (!commentController || !commentController.reactionHandler) {
|
|
return Promise.resolve(undefined);
|
|
}
|
|
|
|
return asPromise(() => {
|
|
const commentThread = commentController.getCommentThread(threadHandle);
|
|
if (commentThread) {
|
|
const vscodeComment = commentThread.getCommentByUniqueId(comment.uniqueIdInThread);
|
|
|
|
if (commentController !== undefined && vscodeComment) {
|
|
if (commentController.reactionHandler) {
|
|
return commentController.reactionHandler(vscodeComment, convertFromReaction(reaction));
|
|
}
|
|
}
|
|
}
|
|
|
|
return Promise.resolve(undefined);
|
|
});
|
|
}
|
|
dispose() {
|
|
|
|
}
|
|
}
|
|
|
|
export class ExtHostCommentThread implements vscode.CommentThread {
|
|
private static _handlePool: number = 0;
|
|
readonly handle = ExtHostCommentThread._handlePool++;
|
|
public commentHandle: number = 0;
|
|
|
|
set threadId(id: string) {
|
|
this._id = id;
|
|
}
|
|
|
|
get threadId(): string {
|
|
return this._id!;
|
|
}
|
|
|
|
get id(): string {
|
|
return this._id!;
|
|
}
|
|
|
|
get resource(): vscode.Uri {
|
|
return this._uri;
|
|
}
|
|
|
|
get uri(): vscode.Uri {
|
|
return this._uri;
|
|
}
|
|
|
|
private readonly _onDidUpdateCommentThread = new Emitter<void>();
|
|
readonly onDidUpdateCommentThread = this._onDidUpdateCommentThread.event;
|
|
|
|
set range(range: vscode.Range) {
|
|
if (!range.isEqual(this._range)) {
|
|
this._range = range;
|
|
this._onDidUpdateCommentThread.fire();
|
|
}
|
|
}
|
|
|
|
get range(): vscode.Range {
|
|
return this._range;
|
|
}
|
|
|
|
private _label: string | undefined;
|
|
|
|
get label(): string | undefined {
|
|
return this._label;
|
|
}
|
|
|
|
set label(label: string | undefined) {
|
|
this._label = label;
|
|
this._onDidUpdateCommentThread.fire();
|
|
}
|
|
|
|
private _contextValue: string | undefined;
|
|
|
|
get contextValue(): string | undefined {
|
|
return this._contextValue;
|
|
}
|
|
|
|
set contextValue(context: string | undefined) {
|
|
this._contextValue = context;
|
|
this._onDidUpdateCommentThread.fire();
|
|
}
|
|
|
|
get comments(): vscode.Comment[] {
|
|
return this._comments;
|
|
}
|
|
|
|
set comments(newComments: vscode.Comment[]) {
|
|
this._comments = newComments;
|
|
this._onDidUpdateCommentThread.fire();
|
|
}
|
|
|
|
private _collapseState?: vscode.CommentThreadCollapsibleState;
|
|
|
|
get collapsibleState(): vscode.CommentThreadCollapsibleState {
|
|
return this._collapseState!;
|
|
}
|
|
|
|
set collapsibleState(newState: vscode.CommentThreadCollapsibleState) {
|
|
this._collapseState = newState;
|
|
this._onDidUpdateCommentThread.fire();
|
|
}
|
|
|
|
private _localDisposables: types.Disposable[];
|
|
|
|
private _isDiposed: boolean;
|
|
|
|
public get isDisposed(): boolean {
|
|
return this._isDiposed;
|
|
}
|
|
|
|
private _commentsMap: Map<vscode.Comment, number> = new Map<vscode.Comment, number>();
|
|
|
|
private _acceptInputDisposables = new MutableDisposable<DisposableStore>();
|
|
|
|
constructor(
|
|
private _proxy: MainThreadCommentsShape,
|
|
private _commentController: ExtHostCommentController,
|
|
private _id: string | undefined,
|
|
private _uri: vscode.Uri,
|
|
private _range: vscode.Range,
|
|
private _comments: vscode.Comment[],
|
|
extensionId: ExtensionIdentifier
|
|
) {
|
|
this._acceptInputDisposables.value = new DisposableStore();
|
|
|
|
if (this._id === undefined) {
|
|
this._id = `${_commentController.id}.${this.handle}`;
|
|
}
|
|
|
|
this._proxy.$createCommentThread(
|
|
this._commentController.handle,
|
|
this.handle,
|
|
this._id,
|
|
this._uri,
|
|
extHostTypeConverter.Range.from(this._range),
|
|
extensionId
|
|
);
|
|
|
|
this._localDisposables = [];
|
|
this._isDiposed = false;
|
|
|
|
this._localDisposables.push(this.onDidUpdateCommentThread(() => {
|
|
this.eventuallyUpdateCommentThread();
|
|
}));
|
|
|
|
// set up comments after ctor to batch update events.
|
|
this.comments = _comments;
|
|
}
|
|
|
|
|
|
@debounce(100)
|
|
eventuallyUpdateCommentThread(): void {
|
|
if (this._isDiposed) {
|
|
return;
|
|
}
|
|
|
|
if (!this._acceptInputDisposables.value) {
|
|
this._acceptInputDisposables.value = new DisposableStore();
|
|
}
|
|
|
|
const commentThreadRange = extHostTypeConverter.Range.from(this._range);
|
|
const label = this.label;
|
|
const contextValue = this.contextValue;
|
|
const comments = this._comments.map(cmt => { return convertToModeComment(this, this._commentController, cmt, this._commentsMap); });
|
|
const collapsibleState = convertToCollapsibleState(this._collapseState);
|
|
|
|
this._proxy.$updateCommentThread(
|
|
this._commentController.handle,
|
|
this.handle,
|
|
this._id!,
|
|
this._uri,
|
|
commentThreadRange,
|
|
label,
|
|
contextValue,
|
|
comments,
|
|
collapsibleState
|
|
);
|
|
}
|
|
|
|
getCommentByUniqueId(uniqueId: number): vscode.Comment | undefined {
|
|
for (let key of this._commentsMap) {
|
|
let comment = key[0];
|
|
let id = key[1];
|
|
if (uniqueId === id) {
|
|
return comment;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
dispose() {
|
|
this._isDiposed = true;
|
|
this._acceptInputDisposables.dispose();
|
|
this._localDisposables.forEach(disposable => disposable.dispose());
|
|
this._proxy.$deleteCommentThread(
|
|
this._commentController.handle,
|
|
this.handle
|
|
);
|
|
}
|
|
}
|
|
|
|
type ReactionHandler = (comment: vscode.Comment, reaction: vscode.CommentReaction) => Promise<void>;
|
|
|
|
class ExtHostCommentController implements vscode.CommentController {
|
|
get id(): string {
|
|
return this._id;
|
|
}
|
|
|
|
get label(): string {
|
|
return this._label;
|
|
}
|
|
|
|
public get handle(): number {
|
|
return this._handle;
|
|
}
|
|
|
|
private _threads: Map<number, ExtHostCommentThread> = new Map<number, ExtHostCommentThread>();
|
|
commentingRangeProvider?: vscode.CommentingRangeProvider;
|
|
|
|
private _reactionHandler?: ReactionHandler;
|
|
|
|
get reactionHandler(): ReactionHandler | undefined {
|
|
return this._reactionHandler;
|
|
}
|
|
|
|
set reactionHandler(handler: ReactionHandler | undefined) {
|
|
this._reactionHandler = handler;
|
|
|
|
this._proxy.$updateCommentControllerFeatures(this.handle, { reactionHandler: !!handler });
|
|
}
|
|
|
|
constructor(
|
|
private _extension: IExtensionDescription,
|
|
private _handle: number,
|
|
private _proxy: MainThreadCommentsShape,
|
|
private _id: string,
|
|
private _label: string
|
|
) {
|
|
this._proxy.$registerCommentController(this.handle, _id, _label);
|
|
}
|
|
|
|
createCommentThread(resource: vscode.Uri, range: vscode.Range, comments: vscode.Comment[]): vscode.CommentThread;
|
|
createCommentThread(arg0: vscode.Uri | string, arg1: vscode.Uri | vscode.Range, arg2: vscode.Range | vscode.Comment[], arg3?: vscode.Comment[]): vscode.CommentThread {
|
|
if (typeof arg0 === 'string') {
|
|
const commentThread = new ExtHostCommentThread(this._proxy, this, arg0, arg1 as vscode.Uri, arg2 as vscode.Range, arg3 as vscode.Comment[], this._extension.identifier);
|
|
this._threads.set(commentThread.handle, commentThread);
|
|
return commentThread;
|
|
} else {
|
|
const commentThread = new ExtHostCommentThread(this._proxy, this, undefined, arg0 as vscode.Uri, arg1 as vscode.Range, arg2 as vscode.Comment[], this._extension.identifier);
|
|
this._threads.set(commentThread.handle, commentThread);
|
|
return commentThread;
|
|
}
|
|
}
|
|
|
|
$createCommentThreadTemplate(uriComponents: UriComponents, range: IRange): ExtHostCommentThread {
|
|
const commentThread = new ExtHostCommentThread(this._proxy, this, undefined, URI.revive(uriComponents), extHostTypeConverter.Range.to(range), [], this._extension.identifier);
|
|
commentThread.collapsibleState = modes.CommentThreadCollapsibleState.Expanded;
|
|
this._threads.set(commentThread.handle, commentThread);
|
|
return commentThread;
|
|
}
|
|
|
|
$updateCommentThreadTemplate(threadHandle: number, range: IRange): void {
|
|
let thread = this._threads.get(threadHandle);
|
|
if (thread) {
|
|
thread.range = extHostTypeConverter.Range.to(range);
|
|
}
|
|
}
|
|
|
|
$deleteCommentThread(threadHandle: number): void {
|
|
let thread = this._threads.get(threadHandle);
|
|
|
|
if (thread) {
|
|
thread.dispose();
|
|
}
|
|
|
|
this._threads.delete(threadHandle);
|
|
}
|
|
|
|
getCommentThread(handle: number): ExtHostCommentThread | undefined {
|
|
return this._threads.get(handle);
|
|
}
|
|
|
|
dispose(): void {
|
|
this._threads.forEach(value => {
|
|
value.dispose();
|
|
});
|
|
|
|
this._proxy.$unregisterCommentController(this.handle);
|
|
}
|
|
}
|
|
|
|
function convertToModeComment(thread: ExtHostCommentThread, commentController: ExtHostCommentController, vscodeComment: vscode.Comment, commentsMap: Map<vscode.Comment, number>): modes.Comment {
|
|
let commentUniqueId = commentsMap.get(vscodeComment)!;
|
|
if (!commentUniqueId) {
|
|
commentUniqueId = ++thread.commentHandle;
|
|
commentsMap.set(vscodeComment, commentUniqueId);
|
|
}
|
|
|
|
const iconPath = vscodeComment.author && vscodeComment.author.iconPath ? vscodeComment.author.iconPath.toString() : undefined;
|
|
|
|
return {
|
|
mode: vscodeComment.mode,
|
|
contextValue: vscodeComment.contextValue,
|
|
uniqueIdInThread: commentUniqueId,
|
|
body: extHostTypeConverter.MarkdownString.from(vscodeComment.body),
|
|
userName: vscodeComment.author.name,
|
|
userIconPath: iconPath,
|
|
label: vscodeComment.label,
|
|
commentReactions: vscodeComment.reactions ? vscodeComment.reactions.map(reaction => convertToReaction(reaction)) : undefined
|
|
};
|
|
}
|
|
|
|
function convertToReaction(reaction: vscode.CommentReaction): modes.CommentReaction {
|
|
return {
|
|
label: reaction.label,
|
|
iconPath: reaction.iconPath ? extHostTypeConverter.pathOrURIToURI(reaction.iconPath) : undefined,
|
|
count: reaction.count,
|
|
hasReacted: reaction.authorHasReacted,
|
|
};
|
|
}
|
|
|
|
function convertFromReaction(reaction: modes.CommentReaction): vscode.CommentReaction {
|
|
return {
|
|
label: reaction.label || '',
|
|
count: reaction.count || 0,
|
|
iconPath: reaction.iconPath ? URI.revive(reaction.iconPath) : '',
|
|
authorHasReacted: reaction.hasReacted || false
|
|
};
|
|
}
|
|
|
|
function convertToCollapsibleState(kind: vscode.CommentThreadCollapsibleState | undefined): modes.CommentThreadCollapsibleState {
|
|
if (kind !== undefined) {
|
|
switch (kind) {
|
|
case types.CommentThreadCollapsibleState.Expanded:
|
|
return modes.CommentThreadCollapsibleState.Expanded;
|
|
case types.CommentThreadCollapsibleState.Collapsed:
|
|
return modes.CommentThreadCollapsibleState.Collapsed;
|
|
}
|
|
}
|
|
return modes.CommentThreadCollapsibleState.Collapsed;
|
|
}
|