-
Notifications
You must be signed in to change notification settings - Fork 20
Code discussions #19
Code discussions #19
Changes from all commits
5c5ca51
bffb82d
591f359
4d600e1
ae8217d
fa1eecb
0da5171
97aaabb
63da797
a0fc9c0
cae4364
bfe9910
e0ecee7
77ef314
738e450
0cb8258
6b643f5
4601624
761ef7a
355f5be
1d06e9f
2bab91e
fe638ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,299 @@ | ||
| import vscode from 'vscode' | ||
| import { repoInfo } from './git' | ||
| import { gql, mutateGraphQL, queryGraphQL } from './graphql' | ||
| import { log } from './log' | ||
|
|
||
| export function activateComments(context: vscode.ExtensionContext): void { | ||
| if (!vscode.workspace.getConfiguration('sourcegraph').get<string>('discussionsPreviewEnabled')) { | ||
| return | ||
| } | ||
| log.appendLine('Discussions preview is enabled') | ||
| const commentProvider = new CommentProvider() | ||
| context.subscriptions.push(vscode.workspace.registerDocumentCommentProvider(commentProvider)) | ||
| } | ||
|
|
||
| class CommentProvider implements vscode.DocumentCommentProvider { | ||
| public async provideDocumentComments( | ||
| document: vscode.TextDocument, | ||
| token: vscode.CancellationToken | ||
| ): Promise<vscode.CommentInfo> { | ||
| let threads: vscode.CommentThread[] = [] | ||
| try { | ||
| threads = await provideDocumentComments(document) | ||
| } catch (e) { | ||
| log.appendLine(`provideDocumentComments ${e}`) | ||
| } | ||
|
|
||
| // commentingRanges are the ranges where the user can create a new comment. | ||
| // For now, this is the entire document. In the future we may wish to limit this | ||
| // in certain ways (e.g. not comment on lines which don't blame to a git revision that has been pushed to a remote). | ||
| const lastLine = document.lineCount - 1 | ||
| const commentingRanges = [new vscode.Range(0, 0, lastLine, document.lineAt(lastLine).range.end.character)] | ||
|
emidoots marked this conversation as resolved.
|
||
|
|
||
| return { threads, commentingRanges } | ||
| } | ||
|
|
||
| public async createNewCommentThread( | ||
| document: vscode.TextDocument, | ||
| range: vscode.Range, | ||
| text: string, | ||
| token: vscode.CancellationToken | ||
| ): Promise<vscode.CommentThread> { | ||
| return await createNewCommentThread(document, range, text) | ||
| } | ||
|
|
||
| public async replyToCommentThread( | ||
| document: vscode.TextDocument, | ||
| range: vscode.Range, | ||
| commentThread: vscode.CommentThread, | ||
| text: string, | ||
| token: vscode.CancellationToken | ||
| ): Promise<vscode.CommentThread> { | ||
| return await replyToCommentThread(document, range, commentThread, text) | ||
| } | ||
|
|
||
| private didChangeCommentThreads = new vscode.EventEmitter<vscode.CommentThreadChangedEvent>() | ||
|
|
||
| public onDidChangeCommentThreads = this.didChangeCommentThreads.event | ||
| } | ||
|
|
||
| async function provideDocumentComments(document: vscode.TextDocument): Promise<vscode.CommentThread[]> { | ||
| log.appendLine(`provideDocumentComments ${document.uri}`) | ||
| const [remoteUrl, branch, path] = await repoInfo(document.fileName) | ||
|
emidoots marked this conversation as resolved.
|
||
| if (!remoteUrl) { | ||
| throw new Error('Git repository has no remote url configured') | ||
| } | ||
| const data = await queryGraphQL( | ||
| gql` | ||
| query DiscussionThreads( | ||
| $targetRepositoryGitCloneURL: String! | ||
| $targetRepositoryPath: String! | ||
| $relativeRev: String! | ||
| ) { | ||
| discussionThreads( | ||
| first: 10000 | ||
| targetRepositoryGitCloneURL: $targetRepositoryGitCloneURL | ||
| targetRepositoryPath: $targetRepositoryPath | ||
| ) { | ||
| totalCount | ||
| pageInfo { | ||
| hasNextPage | ||
| } | ||
| nodes { | ||
| ...DiscussionThreadFields | ||
| } | ||
| } | ||
| } | ||
| ${discussionThreadFieldsFragment} | ||
| `, | ||
| { | ||
| targetRepositoryGitCloneURL: remoteUrl, | ||
| targetRepositoryPath: path, | ||
| relativeRev: branch, | ||
| } | ||
| ) | ||
|
|
||
| if (!data.discussionThreads || !data.discussionThreads.nodes) { | ||
| throw new Error(`Invalid GraphQL response for DiscussionThreads`) | ||
| } | ||
|
|
||
| const threads: vscode.CommentThread[] = [] | ||
| for (const thread of data.discussionThreads.nodes) { | ||
|
nicksnyder marked this conversation as resolved.
|
||
| if (thread.target.__typename !== 'DiscussionThreadTargetRepo') { | ||
| continue | ||
| } | ||
| // TODO: this assumes there is no diff between the document state and the revision | ||
| const sel = thread.target.relativeSelection | ||
| if (sel) { | ||
| const range = new vscode.Range(sel.startLine, sel.startCharacter, sel.endLine, sel.endCharacter) | ||
| threads.push(discussionToCommentThread(document, range, thread)) | ||
| } | ||
| } | ||
|
|
||
| return threads | ||
| } | ||
|
|
||
| async function createNewCommentThread( | ||
| document: vscode.TextDocument, | ||
| range: vscode.Range, | ||
| text: string | ||
| ): Promise<vscode.CommentThread> { | ||
| log.appendLine(`createNewCommentThread ${document.uri} ${range}`) | ||
| const [remoteUrl, branch, path] = await repoInfo(document.fileName) | ||
|
emidoots marked this conversation as resolved.
|
||
| if (!remoteUrl) { | ||
| throw new Error('Git repository has no remote url configured') | ||
| } | ||
|
|
||
| const selection = getSelection(document, range) | ||
| const input: SourcegraphGQL.IDiscussionThreadCreateInput = { | ||
| title: text, | ||
| contents: text, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Either we need to parse the titles out here now (like we do in the webapp), or I need to fix title handling on the backend before we start using this to create discussions on dogfood/prod.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah this was a todo I forgot about. My preference is for "title" to be optional and for the backend to handle creating the title in that case.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I agree, I'll try to make this change tomorrow or over the weekend. |
||
| targetRepo: { | ||
| repositoryGitCloneURL: remoteUrl, | ||
| path, | ||
| branch, | ||
| selection, | ||
| }, | ||
| } | ||
|
|
||
| const data = await mutateGraphQL( | ||
| gql` | ||
| mutation CreateThread($input: DiscussionThreadCreateInput!, $relativeRev: String!) { | ||
| discussions { | ||
| createThread(input: $input) { | ||
| ...DiscussionThreadFields | ||
| } | ||
| } | ||
| } | ||
| ${discussionThreadFieldsFragment} | ||
| `, | ||
| { input, relativeRev: branch } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| ) | ||
|
|
||
| if (!data.discussions || !data.discussions.createThread) { | ||
| throw new Error(`Invalid GraphQL response for CreateThread`) | ||
| } | ||
|
|
||
| return discussionToCommentThread(document, range, data.discussions.createThread) | ||
| } | ||
|
|
||
| async function replyToCommentThread( | ||
| document: vscode.TextDocument, | ||
| range: vscode.Range, | ||
| thread: vscode.CommentThread, | ||
| text: string | ||
| ): Promise<vscode.CommentThread> { | ||
| const [, branch] = await repoInfo(document.fileName) | ||
|
emidoots marked this conversation as resolved.
|
||
| const data = await mutateGraphQL( | ||
| gql` | ||
| mutation AddCommentToThread($threadID: ID!, $contents: String!, $relativeRev: String!) { | ||
| discussions { | ||
| addCommentToThread(threadID: $threadID, contents: $contents) { | ||
| ...DiscussionThreadFields | ||
| } | ||
| } | ||
| } | ||
| ${discussionThreadFieldsFragment} | ||
| `, | ||
| { threadID: thread.threadId, contents: text, relativeRev: branch } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same thing here, |
||
| ) | ||
|
|
||
| if (!data.discussions || !data.discussions.addCommentToThread) { | ||
| throw new Error(`Invalid GraphQL response for AddCommentToThread`) | ||
| } | ||
|
|
||
| return discussionToCommentThread(document, range, data.discussions.addCommentToThread) | ||
| } | ||
| function discussionToCommentThread( | ||
| document: vscode.TextDocument, | ||
| range: vscode.Range, | ||
| thread: SourcegraphGQL.IDiscussionThread | ||
| ): vscode.CommentThread { | ||
| const comments = thread.comments.nodes.map(comment => ({ | ||
| commentId: comment.id, | ||
| body: new vscode.MarkdownString(comment.contents), | ||
| userName: comment.author.username, | ||
| gravatar: comment.author.avatarURL || '', | ||
| })) | ||
|
|
||
| return { | ||
| threadId: thread.id, | ||
| resource: document.uri, | ||
| range, | ||
| comments, | ||
| } | ||
| } | ||
|
|
||
| function getSelection( | ||
| document: vscode.TextDocument, | ||
| range: vscode.Range | ||
| ): SourcegraphGQL.IDiscussionThreadTargetRepoSelectionInput { | ||
| const beforeRange = new vscode.Range(range.start.line - 3, 0, range.start.line - 1, 0) | ||
| const linesBefore = getLines(document, beforeRange) | ||
|
|
||
| const lines = getLines(document, range) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe the capturing of context lines here is wrong. This is one of the issues I saw in your PR before but I didn't have time to investigate it. Additionally, before we start using this against dogfood/prod I need to figure out whether or not you should actually be specifying potentially modified context lines here AND specifying an exact Git revision, or if you should be doing something else in that situation. Otherwise, this will leave our data in an inconsistent state and it will be hard to deal with.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Do you mean the logic is wrong, or do you mean we shouldn't be capturing them at all? VS Code does have a case that web doesn't have to deal with: unpushed (or even unsaved) modifications. If we only want to support discussing committed/pushed code (which is fine), then there is some work to be done to limit the commenting ranges (as discussed in another comment).
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When I last tested, the logic was wrong in some way (but I forget exactly how. I will check / reconfirm this logic is solid and that we're capturing what I expect here. I'll comment back here once I've done this. I've been thinking about the issue of unpushed/unsaved modifications, and I see a few options (in no particular order):
Of the above I think option 2 would be the best, and probably relatively simple to implement.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I agree and I wrote similar logic before in the old Sourcegraph Editor comments implementation. I think it reduces to sending the newest commit hash that the selected lines blame to. If this includes uncommitted lines, then the hash is either omitted or uses a sentinel value
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using the sentinel value
I agree, this is the best approach. It also means zero work here for that case. TODO:
To merge, I am fine with something as simple as "If the file is at all modified, we send commit
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
No, but it is basically the commit hash of only zeros so forty zeros is the "canonical" value (everything else is just an abbreviation like you can do with any commit hash). Do you prefer this sentinel value over just not sending a value at all (the later might be more intuitive from an API perspective)?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Jeez, thank you for questioning me on that. This is exactly why I originally made the field nullable, so yeah, we should just send null in that case (we can/should still send the branch name though):
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I came here to ask too if we can just use null. Sentinel values are bad for type safety. |
||
|
|
||
| const afterRange = new vscode.Range(range.end.line + 1, 0, range.end.line + 3, 0) | ||
| const linesAfter = getLines(document, afterRange) | ||
|
|
||
| return { | ||
| startLine: range.start.line, | ||
| startCharacter: range.start.character, | ||
| endLine: range.end.line, | ||
| endCharacter: range.end.character, | ||
| linesBefore, | ||
| lines, | ||
| linesAfter, | ||
| } | ||
| } | ||
|
|
||
| function getLines(document: vscode.TextDocument, range: vscode.Range): string[] { | ||
| const lines: string[] = [] | ||
| for (let i = range.start.line; i <= range.end.line; i++) { | ||
| if (i >= 0 && i < document.lineCount) { | ||
| lines.push(document.lineAt(i).text) | ||
| } | ||
| } | ||
| return lines | ||
| } | ||
|
|
||
| const discussionThreadFieldsFragment = gql` | ||
| fragment DiscussionThreadFields on DiscussionThread { | ||
| id | ||
| author { | ||
| ...UserFields | ||
| } | ||
| comments { | ||
| totalCount | ||
| nodes { | ||
| id | ||
| author { | ||
| ...UserFields | ||
| } | ||
| contents | ||
| } | ||
| } | ||
| title | ||
| target { | ||
| __typename | ||
| ... on DiscussionThreadTargetRepo { | ||
| repository { | ||
| name | ||
| } | ||
| path | ||
| relativePath(rev: $relativeRev) | ||
| branch { | ||
| displayName | ||
| } | ||
| revision { | ||
| displayName | ||
| } | ||
| selection { | ||
| startLine | ||
| startCharacter | ||
| endLine | ||
| endCharacter | ||
| linesBefore | ||
| lines | ||
| linesAfter | ||
| } | ||
| relativeSelection(rev: $relativeRev) { | ||
| startLine | ||
| startCharacter | ||
| endLine | ||
| endCharacter | ||
| } | ||
| } | ||
| } | ||
| inlineURL | ||
| createdAt | ||
| updatedAt | ||
| archivedAt | ||
| } | ||
|
|
||
| fragment UserFields on User { | ||
| displayName | ||
| username | ||
| avatarURL | ||
| } | ||
| ` | ||
Uh oh!
There was an error while loading. Please reload this page.