Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Comments API refactor #68020

Closed
rebornix opened this issue Feb 7, 2019 · 14 comments
Closed

Comments API refactor #68020

rebornix opened this issue Feb 7, 2019 · 14 comments
Assignees
Labels
api-finalization api-proposal comments Comments Provider/Widget/Panel issues
Milestone

Comments

@rebornix
Copy link
Member

rebornix commented Feb 7, 2019

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

@rebornix rebornix transferred this issue from another repository Feb 7, 2019
@kieferrm kieferrm assigned rebornix and RMacfarlane and unassigned jrieken Feb 7, 2019
@rebornix rebornix added the comments Comments Provider/Widget/Panel issues label Feb 8, 2019
@eamodio
Copy link
Contributor

eamodio commented Feb 13, 2019

@rebornix Would extensions be able to forgo the visible CommentControl and provide their own implementation (say via the webview) and still participate with the rest of the commenting system?

@rebornix
Copy link
Member Author

@eamodio do you mean allowing extensions to provide their own implementation of the Comment Widget in the editor?

@eamodio
Copy link
Contributor

eamodio commented Feb 14, 2019

While that would be nice -- I'm assuming that is a no-go. But alternatively something that in response to someone trying to open/edit a comment an extension could provide an alternative hook. For example, CodeStream today lets you comment on code, but our comment box lives inside our webview (for many reasons) -- but if we could still use the comment + button in the editor to trigger our comment box (in our webview) that would be quite helpful

@rebornix
Copy link
Member Author

@eamodio if I understand correctly, taking what we have implemented in VS Code into consideration, what you ask you is more like

  • Users still see comments from CodeStream in Comments Panel
  • Users can click on the gutter to create a comment, but it goes to the CodeStream's WebviewPanelSerializer.

Did I miss anything?

@eamodio
Copy link
Contributor

eamodio commented Feb 14, 2019

@rebornix yeah mostly

  • Users can see CodeStream comments in the Comments Panel
  • Users can click on a comment in the Comments Panel to jump to the code and open CodeStream's webview to the comment
  • Users can see CodeStream comments in the gutter (although ideally would like to customize that indicator/button much more)
  • Users can click on an existing comment in the gutter to open the comment in CodeStream's webview
  • Users can click on the gutter to create a comment via CodeStream's webview

Alternatively providing much more control to the CommentWidget itself (host a mini webview?) and also some control over the rendering of the comment itself when not editing?

@rebornix
Copy link
Member Author

We finished the major part of refactoring and the new SCM like did make authoring easier than before based on my experience on rewriting GH PR commenting support (will send out pr lately), below is the first draft of the new API.

interface CommentThread {
	threadId: string;
	resource: Uri;
	range: Range;
	comments: Comment[];
	acceptInputCommands?: Command[];
	collapsibleState?: CommentThreadCollapsibleState;
	dispose?(): void;
}

interface Comment {
	commentId: string;
	body: MarkdownString;
	userName: string;
	userIconPath?: Uri;
	/**
	 * The command to be executed if the comment is selected in the Comments Panel
	 */
	command?: Command;
	editCommand?: Command;
	deleteCommand?: Command;
	isDraft?: boolean;
	commentReactions?: CommentReaction[];
}

export interface CommentWidget {
	/*
	 * Comment thread in this Comment Widget
	 */
	commentThread: CommentThread;

	/*
	 * Textarea content in the comment widget.
	 * There is only one active input box in a comment widget.
	 */
	input: string;
}

export interface CommentControl {
	readonly id: string;
	readonly label: string;
	/**
	 * The active (focused) comment widget.
	 */
	readonly widget?: CommentWidget;
	/**
	 * The active range users attempt to create comments against.
	 */
	readonly activeCommentingRange?: Range;
	createCommentThread(id: string, resource: Uri, range: Range, comments: Comment[], acceptInputCommands: Command[], collapsibleState?: CommentThreadCollapsibleState): CommentThread;
	createCommentingRanges(resource: Uri, ranges: Range[], newCommentThreadCommand: Command): CommentingRanges;
	dispose(): void;
}

namespace comment {
	export function createCommentControl(id: string, label: string): CommentControl;
}

Now the extension is responsible for creating the comment threads when an editor is being opened or the extension detects the current workspace is in review mode. All comment threads registered will show up in Comments Panel and disappear when they are disposed.

CommentThread can contribute multiple acceptInputCommands and the core will display all of them at the bottom of corresponding comment widget. Here is some pseudo code demonstrating how the extension uses acceptInputCommands to create a comment and start a review

const commentControl = vscode.comment.createCommentControl('ghpr', 'GitHub PullRequest');

const startReviewCommand = {
  title: 'Start Review',
  command: 'ghpr.startReview',
  arguments: [ commentControl ];
}

...
const thread = commentControl.createCommentThread(id, resource, range, [],[startReviewCommand]);

vscode.commands.registerCommand('ghpr.startReview', (commentControl) => {
  let input = commentControl.widget.input;
  let currentCommentThread = commentControl.widget.commentThread;

  // start review ...
});

cc @RMacfarlane

@rebornix rebornix added this to the March 2019 milestone Mar 4, 2019
@rebornix
Copy link
Member Author

rebornix commented Mar 4, 2019

As we discussed last Friday and today's API syncup, we are going to simply the API a bit more

widget?: CommentWidget;

As extensions have access to the comment thread when creating the commands, we don't need widget actually. The only thing we care about now is just the content from the textarea, so we are going to change it to InputBox

createCommentThread(id: string, resource: Uri, range: Range, comments: Comment[], acceptInputCommands: Command[], collapsibleState?: CommentThreadCollapsibleState): CommentThread;

Any optional property should not be in constructor.

createCommentingRanges()

Instead of this explicit function, we can ask extensions to provide a commentingRangeProvider and a default createCommentCommand.

@eamodio
Copy link
Contributor

eamodio commented Mar 5, 2019

@rebornix Given the current proposed addition of CodeInsets I think it would be great to ensure that this new api could support an extension that takes advantage of both features together. For example, using the create comment button to open a CodeInset at that line which could collect the comment input (however the extension wanted via the inset), and then be able to "submit" (and cancel) the inset to either complete or cancel the creation of a comment. Ideally the same for replying and editing.

Basically ensuring enough hook points and control over the ui comment points and a programmatic api to handle the data.

@rebornix
Copy link
Member Author

rebornix commented Mar 11, 2019

Extension example demonstrating how the API can be used microsoft/vscode-extension-samples#160

@prkb
Copy link

prkb commented Mar 12, 2019

@rebornix Are there any plans to support nested comments as part of this API similar to the pull request comments on Bitbucket (eg. https://bitbucket.org/pbusam/test-repo/pull-requests/1).

@rebornix rebornix mentioned this issue Mar 16, 2019
2 tasks
@rebornix
Copy link
Member Author

@prkb thanks for the feedback. we don't have plan for nested comments right now.

@IlyaBiryukov
Copy link

@rebornix Is WorkspaceCommentProvider not going to change?

@rebornix
Copy link
Member Author

rebornix commented Apr 2, 2019

@IlyaBiryukov WorkspaceCommentProvider will be removed.

@IlyaBiryukov
Copy link

@rebornix How the extension can populate the comment panel then? WorkspaceCommentProvider used to be the one that populates the panel.

@rebornix rebornix modified the milestones: April 2019, On Deck May 10, 2019
@vscodebot vscodebot bot locked and limited conversation to collaborators Sep 7, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-finalization api-proposal comments Comments Provider/Widget/Panel issues
Projects
None yet
Development

No branches or pull requests

7 participants