-
Notifications
You must be signed in to change notification settings - Fork 34.9k
Description
TLDR
Read Proposals
We have been discussing how we can make the commenting api generic enough but also have the flexibility of rendering. With this in mind, we proposed the first version of the commenting api, which includes two types of CommentProvider
interface DocumentCommentProvider {
provideDocumentComments(document: TextDocument, token: CancellationToken): Promise<CommentInfo>;
onDidChangeCommentThreads: Event<CommentThreadChangedEvent>;
}
interface WorkspaceCommentProvider {
provideWorkspaceComments(token: CancellationToken): Promise<CommentThread[]>;
onDidChangeCommentThreads: Event<CommentThreadChangedEvent>;
}
DocumentCommentProvider
is invoked when a file is opened in an editor while WorkspaceCommentProvider
is used to feed the Comments Panel. Both of these two providers will provide CommentThread
to the core
interface CommentThread {
threadId: string;
resource: uri;
range: range;
comments: comment[];
collapsibleState?: commentThreadCollapsibleState;
}
CommentThread
is simply an abstraction of how comments are being rendered in the editor, we can see it as a flat list of Comment
created at the same position in a particular document. It has nothing to do with how comments are being grouped into Discussion or Review or whatever, the underlying business logic is up to extensions.
Patterns
So far so good. Besides the API looks pretty close to our language feature apis, and it's easy to understand and adopt
export function registerCompletionItemProvider(selector: DocumentSelector, provider: CompletionItemProvider, ...triggerCharacters: string[]): Disposable;
export interface CompletionItemProvider {
provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): ProviderResult<CompletionItem[] | CompletionList>;
resolveCompletionItem?(item: CompletionItem, token: CancellationToken): ProviderResult<CompletionItem>;
}
However, displaying comments in the comments panel and editor is just a start, we need to allow users to create new comments and reply to comments, and furthermore, users should be able to edit and delete comments.
We went back and forth on supporting comments creation and reply. The UI part is kind of intuitive, it consists of a textarea and a button
But how can extensions access the content in the textarea? What text should we render on the button, "Add Comment" or "Start Review"? The first idea came to our mind is using Command, it's a commonly used pattern in our extension API.
export class TreeItem {
...
/**
* The [command](#Command) that should be executed when the tree item is selected.
*/
command?: Command;
...
}
export class CodeLens {
...
/**
* The command this code lens represents.
*/
command?: Command;
...
}
export class CodeAction {
...
/**
* The command this code lens represents.
*/
command?: Command;
...
}
We are wondering if we can have a command
property on every CommentThread
which implements Reply
logic and a command
for the CommentProvider
which implements Create Comment
. But it turns out we have more and more open questions.
One good example is, as an extension author, when you attempt to implement the Reply
command, how do you know what kind of parameters VSCode will pass in, in what sequence? For comment reply, we only need the related comment thread, the text in the textarea. While for comment creation, we need document uri, the position or range of the newly created comment and content in textarea. Even though they are all Command
but they are in differnet shapes. Instead of digging into this pattern too much, we introduced declractive APIs like below
interface DocumentCommentProvider {
...
createNewCommentThread(document: TextDcument, range: Range, text: string, token: CancellationToken): Promise<CommentThread>;
replyToCommentThread(document: TextDocument, range: Range, commentThread: CommentThread, text: string, token: CancellationToken): Promise<CommentThread>;
...
}
We are already using this group of APIs in GitHub Pull Request extension and enriched it with EditComment
and DeleteComment
later. But now we have four kinds of buttons/actions on the UI
- Create
- Reply
- Update
- Delete
and we provide expeclit API for them. However when we started design of commenting grouping/review/drafting concept, we found ourselves have to introduce new buttons or action items to the Comment Widget and it's a probably a good time to revisit our API design.
After digging into our existing APIs, we found SourceControl is in the same situation as ours, different source contorl providers have different wording, resource grouping and commands, they need to access the input box in the SCM viewlet. It would be interesting to see if we can similar design as SCM API.
Proposals
A
The first proposal is we still follow our existing APIs and introduce a set of new APIs to start/end draft mode
interface DocumentCommentProvider {
...
startDraft(token: CancellationToken): Promise<void>;
deleteDraft(token: CancellationToken): Promise<void>;
finishDraft(token: CancellationToken): Promise<void>;
...
}
After users click Start Draft
button on the UI, we invoke startDraft
API and all comments being created will be marked as pending
or draft
. The flag or type is added to Comment
interface Comment {
commentId: string;
body: MarkdownString;
userName: string;
userIconPath?: Uri;
gravatar?: string;
canEdit?: boolean;
canDelete?: boolean;
/*
* is draft
*/
isDraft: boolean;
}
VSCode core switch the draft mode (display Finish Draft, or Delete Draft button), once it finds that there is a comment being marked as isDraft: true
.
B
The second proposal is we follow the design of Source Control Provider. To enable commenting support, firstly extensions need to create a CommentProvider
export function createCommentProvider(id: string, label: string, rootUri?: Uri): CommentProvider;
With rootUri
we can have sound support for multi root workspace. The CommentProvider
would have following properties
interface CommentProvider {
readonly id: string;
readonly label: string;
readonly rootUri: Uri | undefined;
/**
* The active (focused) comment widget.
*/
readonly widget: CommentWidget;
/**
* Optional comment template string.
*/
commentTemplate?: string;
/**
* Accept input commands.
*
* The first command will be invoked when the user accepts the value
* in the comment widget textarea.
*/
acceptInputCommands: Command[];
/*
* 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;
}
We will render a dropdown list in the comment widget for all acceptInputCommands
. For example, an extension can register Create Comment
and Start Conversation
commands, and when the command is being invoked, we will pass in CommentProvider
, and then the extension can read the commentWidget
property to access the active/focused comment widget.
interface CommentWidget {
/*
* Comment thread in this Comment Widget
*/
commentThread: ICommentThread;
/*
* Focused Comment
* This comment must be part of CommentWidget.commentThread
*/
comment?: Commnet;
/*
* Textarea content in the comment widget.
* There is only one active input box in a comment widget.
*/
input: string;
}
The active comment widget knows about the current comment thread, the focused comment and input
of the textarea in the widget.
To reply to a comment, the commentThead
is a valid one which has an id, a range, resource uri, etc. However, if you are going to create a new comment thread, commentThread.id
is null
as it doesn't exist yet.
Now the core doesn't need to control the draft/review/conversation mode as the state can be fully managed by the extension. For example, user choose Start Conversation
command from the dropdown list, and then the extension can update CommentProvider.acceptInputCommands
and provide three other commands
- Add to conversation
- Delete conversation
- Submit conversation
The dropdown list on Comment Widget will be updated accordingly.