Skip to content

Comments API refactor #68020

Closed
Closed
@rebornix

Description

@rebornix

When we started brainstorming about supporting Comments in VS Code, we went through existing stable APIs to see if there is any pattern we can follow.

Provider Like

Firstly, we have dozens of languages specific APIs. Even though there are many languages and they vary a lot in syntaxes or semantics, the editor provides an API that makes it simple to provide common features like code navigation, completion or code checking, by having all UI and actions already in place, and by allowing you to participate by providing data only.

For example, to introduce a CodeLens, what you need to do is simply providing a function that can be called with a TextDocument returning CodeLens info. In the CodeLens info, you need to provide a range where the CodeLens should be rendered above, and a [command](VS Code API | Visual Studio Code Extension API) which should be executed when users click the CodeLens.

languages.registerHoverProvider('javascript', {
    provideCodeLenses(document) {
        return {
             range: new Range(0, 0, 0, 1),
             command: /*Custom Command*/
		  }
    }
});

The rest, like tracking the mouse, positioning the CodeLens, re-requesting the CodeLens when document or content changes, etc, is taken care of the editor.

The benefits of having an API similar to Language APIs (or we can call it Provider like APIs, as the API only provides data), is that users can always have the same experience, no matter which provider is regisitered under the hood, as the editor maintains how the UI is being rendered and how users interact with the editor.

The drawback is we need to define all actions explicitly in the APIs, and the actions should be generic enough, for instance, the CodeLens info has one action, and it represents the command which should be executed when users click on the CodeLens.

SCM Like

The other type of API got our interest is SCM. Instead of being Provider Like, SCM API introduces its own Domain Language and has more control of what and when the data should be rendered in the SCM Viewlet.

For example, Git extension listens to the git status change in local repository and when it finds dirty files, it will create ResourceGroup and ResourceGroupState accordingly, which represents Staged Changes and Changes shown in below image.

7838ba7e-ceef-4e70-bf23-bfc4e5c0964c

Git extension also registers commands for ResourceGroup and ResourceGroupState entries it just creates. VS Code will decide which commands to render for each ResourceGroup and ResourceGroupState based on the condition Git extension specifies in contributes as below.

"contribute": {
    "scm/title": [
    ...
        {
          "command": "git.stageAll",
          "group": "4_stage",
          "when": "scmProvider == git"
        },
    ...
    ],
    "scm/resourceState/context": [
        {
            "command": "git.stage",
           "when": "scmProvider == git && scmResourceGroup == merge",
           "group": "1_modification"
        },
    ...
    ]
}

When users commit current code change, Git extension can read the content from the textarea, by accessing inputBox property on SourceControl object it creates (the inputBox is a mirror of the textarea rendered at the top of the SCM Viewlet).

export interface SourceControl {
    readonly inputBox: SourceControlInputBox;
}

The SCM API doesn’t predefine types for file/content changes, and it doesn’t define what actions a user can operate on a file change either. Instead it allows extensions to define themselves and register commands based on its own logic.


The SCM like API is more freeform compared to the Provider Like API but when we start GH PR extension, we don’t know yet how it should look like and how users would love to interact with comments in the editor. To allow us move fast, we started with Provider Like APIs and it works pretty well at the beginning.

We introduced two providers to provide comments data for opened editors and the Comments Panel

interface DocumentCommentProvider {
	provideDocumentComments(document: TextDocument, token: CancellationToken): Promise<CommentInfo>;
}
interface WorkspaceCommentProvider {
	provideWorkspaceComments(token: CancellationToken): Promise<CommentThread[]>; 
}

To support creating comments and replying to comments, we then added two buttons labeled as Add comment and Reply comment, and two more methods into DocumentCommentsProvider

84c88d10-88d9-4e12-a88a-ebf8fadb008a

interface DocumentCommentProvider {
	provideDocumentComments(document: TextDocument, token: CancellationToken): Promise<CommentInfo>;
	createNewCommentThread?(document: TextDocument, range: Range, text: string, token: CancellationToken): Promise<CommentThread>;
	replyToCommentThread?(document: TextDocument, range: Range, commentThread: CommentThread, text: string, token: CancellationToken): Promise<CommentThread>;
}

Since the extension doesn’t have access to the textarea in the Comment Widget, the core will pass in the content in the textarea when it calls createNewCommentThread and replyToCommentThread functions.

The process for adding a feature for Comments was kind of finalized at that moment, we’ll firstly add a button to the Comment Widget, and then add a new method to DocumentCommentProvider. The methods list of DocumentCommentProvider keeps getting longer as a result.

interface DocumentCommentProvider {
	provideDocumentComments(document: TextDocument, token: CancellationToken): Promise<CommentInfo>;
	createNewCommentThread(document: TextDocument, range: Range, text: string, token: CancellationToken): Promise<CommentThread>;
	replyToCommentThread(document: TextDocument, range: Range, commentThread: CommentThread, text: string, token: CancellationToken): Promise<CommentThread>;

	editComment?(document: TextDocument, comment: Comment, text: string, token: CancellationToken): Promise<void>;
	deleteComment?(document: TextDocument, comment: Comment, token: CancellationToken): Promise<void>;

	startDraft?(document: TextDocument, token: CancellationToken): Promise<void>;
	deleteDraft?(document: TextDocument, token: CancellationToken): Promise<void>;
	finishDraft?(document: TextDocument, token: CancellationToken): Promise<void>;
	
	startDraftLabel?: string;
	deleteDraftLabel?: string;
	finishDraftLabel?: string;
	
	addReaction?(document: TextDocument, comment: Comment, reaction: CommentReaction): Promise<void>;
	deleteReaction?(document: TextDocument, comment: Comment, reaction: CommentReaction): Promise<void>;
	reactionGroup?: CommentReaction[];

	onDidChangeCommentThreads: Event<CommentThreadChangedEvent>;
}

Even though we did make the API generic enough but still make quite a few assumptions like, Comment Provider can have a draft system, or it can allow users to post reactions on comments. After doing more investigation and discussions #63609 , we think the Provider Like API can’t serve us well and it’s the best time to move to the SCM like pattern.

Migration Plan

As discussed in Issue #63609 , the new API will allow extension to create a CommentControl and through which we can read the content of the textare in Comment Widget. The way extensions provide comment threads for opened documents doesn’t change too much but now the extensions control what actions should be rendered in a comment thread by providing acceptInputCommands.

interface CommentControl {
	readonly id: string;
	readonly label: string;

	/**
	 * The active (focused) comment widget.
	 */
	readonly widget: CommentWidget;

	/*
	 * Command for updating a comment.
	 */
	updateCommand?: Command;

	/**
	 * Create a new comment thread
	 */
	createCommentThread(id: string, resource: Uri, range: Range): CommentThread;

	/**
	 * Create a new commenting range
	 */
	createCommentingRange(resource: Uri, range: Range): CommentingRange;

	/**
	 * Dispose this comment provider.
	 */
	dispose(): void;
}

interface CommentThread {
  threadId: string;
	resource: uri;
	range: range;
	comments: comment[];
	/**
	 * Accept input commands.
	 *
	 * The first command will be invoked when the user accepts the value
	 * in the comment widget textarea.
	 */
	acceptInputCommands: Command[];
}

When the extension modify CommentControl or CommentThread properties, VS Code will rerender the Comment Widget and Comment Panel accordingly.

We will also add a new section in contributes section in package.json , within which the extension can register specific commands/actions for CommentThread or Comment. For example, GH PR extension can probably implement Reaction as commands.

e74ddb20-b7b1-45b7-90ca-078ccda04047

Related issues

Comments Provider API #53598
CommentProvider draft api proposals #63609

Metadata

Metadata

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions