-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
292 additions
and
5 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,279 @@ | ||
import * as vscode from 'vscode'; | ||
import { VirtualFileSystem } from '../core/remoteFileSystemProvider'; | ||
import { SocketIOAPI } from '../api/socketio'; | ||
import { CommentThreadSchema, DocumentRangesSchema } from '../api/extendedBase'; | ||
|
||
type ReviewDecorationTypes = 'openComment' | 'resolvedComment' | 'insertChange' | 'deleteChange'; | ||
|
||
const reviewDecorationOptions: {[type in ReviewDecorationTypes]: vscode.DecorationRenderOptions} = { | ||
openComment: { | ||
// backgroundColor: 'rgba(243, 177, 17, 0.3)', | ||
backgroundColor: new vscode.ThemeColor('editor.hoverHighlightBackground'), | ||
gutterIconPath: 'resources/gutter-comment-unresolved.svg', | ||
}, | ||
resolvedComment: { | ||
gutterIconPath: 'resources/gutter-pass.svg', | ||
}, | ||
insertChange: { | ||
// color: 'rgba(44, 142, 48, 0.3)', | ||
backgroundColor: new vscode.ThemeColor('diffEditor.insertedTextBackground'), | ||
gutterIconPath: 'resources/gutter-edit.svg', | ||
}, | ||
deleteChange: { | ||
// color: 'rgba(197, 6, 11, 1.0)', | ||
backgroundColor: new vscode.ThemeColor('diffEditor.removedTextBackground'), | ||
gutterIconPath: 'resources/gutter-edit.svg', | ||
}, | ||
}; | ||
|
||
function offsetToRange(document: vscode.TextDocument, offset:number, quotedText: string): vscode.Range { | ||
return new vscode.Range( | ||
document.positionAt(offset), | ||
document.positionAt(offset + quotedText.length), | ||
); | ||
} | ||
|
||
function genThreadMarkdownString(thread: CommentThreadSchema): vscode.MarkdownString { | ||
const text = new vscode.MarkdownString(); | ||
text.isTrusted = true; | ||
text.supportHtml = true; | ||
text.supportThemeIcons = true; | ||
// append thread message | ||
for (const message of thread.messages) { | ||
const username = `${message.user.first_name} ${message.user.last_name||''}`; | ||
const date = new Date(message.timestamp).toLocaleDateString(); | ||
text.appendMarkdown(`**[${username}]()**: ${message.content}\n`); | ||
if (thread.messages.length > 1) { | ||
text.appendMarkdown(`*${date}* • [Edit](command:) • [Delete](command:)`); | ||
} else if (thread.messages.length === 1) { | ||
text.appendMarkdown(`*${date}* • [Edit](command:)`); | ||
} | ||
} | ||
// append possible resolved message | ||
if (thread.resolved) { | ||
const username = `${thread.resolved_by_user?.first_name} ${thread.resolved_by_user?.last_name||''}`; | ||
const date = new Date(thread.resolved_at!).toLocaleDateString(); | ||
text.appendMarkdown(`**[${username}]()**: ${vscode.l10n.t('Mark as resolved')}.\n`); | ||
text.appendMarkdown(`*${date}*`); | ||
} | ||
// append action buttons | ||
if (thread.resolved) { | ||
text.appendMarkdown(` | ||
<table><tr align="center"> | ||
<td>[Resolve](command:)</td> | ||
<td>[Reply](command:)</td> | ||
</tr></table>`); | ||
} else { | ||
text.appendMarkdown(` | ||
<table><tr align="center"> | ||
<td>[Reopen](command:)</td> | ||
<td>[Delete](command:)</td> | ||
</tr></table>`); | ||
} | ||
return text; | ||
} | ||
|
||
export class ReviewPanelProvider { | ||
private reviewDecorationTypes: {[type in ReviewDecorationTypes]: vscode.TextEditorDecorationType}; | ||
private reviewRecords: {[docId:string]: DocumentRangesSchema} = {}; | ||
private reviewThreads: {[threadId:string]: CommentThreadSchema} = {}; | ||
|
||
constructor( | ||
private readonly vfs: VirtualFileSystem, | ||
readonly context: vscode.ExtensionContext, | ||
private readonly socket: SocketIOAPI, | ||
) { | ||
// create decoration types | ||
this.reviewDecorationTypes = { | ||
openComment: vscode.window.createTextEditorDecorationType({ | ||
...reviewDecorationOptions.openComment, | ||
gutterIconPath: context.asAbsolutePath(reviewDecorationOptions.openComment.gutterIconPath as string), | ||
}), | ||
resolvedComment: vscode.window.createTextEditorDecorationType({ | ||
...reviewDecorationOptions.resolvedComment, | ||
gutterIconPath: context.asAbsolutePath(reviewDecorationOptions.resolvedComment.gutterIconPath as string), | ||
}), | ||
insertChange: vscode.window.createTextEditorDecorationType({ | ||
...reviewDecorationOptions.insertChange, | ||
gutterIconPath: context.asAbsolutePath(reviewDecorationOptions.insertChange.gutterIconPath as string), | ||
}), | ||
deleteChange: vscode.window.createTextEditorDecorationType({ | ||
...reviewDecorationOptions.deleteChange, | ||
gutterIconPath: context.asAbsolutePath(reviewDecorationOptions.deleteChange.gutterIconPath as string), | ||
}), | ||
}; | ||
// init review records | ||
this.vfs.getAllDocumentReviews().then((records) => { | ||
const {ranges,threads} = records!; | ||
this.reviewRecords = ranges; | ||
this.reviewThreads = threads; | ||
this.refreshReviewDecorations(); | ||
this.registerEventHandlers(); | ||
}); | ||
} | ||
|
||
private registerEventHandlers() { | ||
this.socket.updateEventHandlers({ | ||
onCommentThreadResolved: (threadId, userInfo) => { | ||
const thread = this.reviewThreads[threadId]; | ||
if (thread) { | ||
thread.resolved = true; | ||
thread.resolved_by_user = userInfo; | ||
thread.resolved_at = new Date().toISOString(); | ||
this.refreshReviewDecorations(thread.doc_id); | ||
} | ||
}, | ||
onCommentThreadReopen: (threadId) => { | ||
const thread = this.reviewThreads[threadId]; | ||
if (thread) { | ||
thread.resolved = false; | ||
thread.resolved_by_user = undefined; | ||
thread.resolved_at = undefined; | ||
this.refreshReviewDecorations(thread.doc_id); | ||
} | ||
}, | ||
onCommentThreadDeleted: (threadId) => { | ||
const thread = this.reviewThreads[threadId]; | ||
if (thread) { | ||
delete this.reviewThreads[threadId]; | ||
this.refreshReviewDecorations(thread.doc_id); | ||
} | ||
}, | ||
onCommentThreadMessageCreated: (threadId, message) => { | ||
const thread = this.reviewThreads[threadId]; | ||
if (thread) { | ||
// case 1: `otUpdateApplied` arrives first | ||
thread.messages.push(message); | ||
this.refreshReviewDecorations(thread.doc_id); | ||
} else { | ||
// case 2: `new-comment` arrives first | ||
this.reviewThreads[threadId] = { | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
doc_id: undefined, // fill in later | ||
messages: [message], | ||
}; | ||
} | ||
}, | ||
onCommentThreadMessageEdited: (threadId, messageId, message) => { | ||
const thread = this.reviewThreads[threadId]; | ||
if (thread) { | ||
const index = thread.messages.findIndex((m) => m.id === messageId); | ||
if (index !== -1) { | ||
thread.messages[index].content = message; | ||
this.refreshReviewDecorations(thread.doc_id); | ||
} | ||
} | ||
}, | ||
// | ||
onFileChanged: (update) => { | ||
if (update.op===undefined) { return; } | ||
// update review records' comments | ||
if (update.op[0].t !== undefined && update.op[0].c !== undefined) { | ||
const docId = update.doc; | ||
const {p,c,t} = update.op[0]; | ||
const userId = update.meta?.user_id || ''; | ||
// create new comment thread if not exists | ||
if (this.reviewRecords[docId] === undefined) { | ||
this.reviewRecords[docId] = {comments: [], changes: []}; | ||
} | ||
let comments = this.reviewRecords[docId]?.comments; | ||
if (comments === undefined) { | ||
comments = []; | ||
this.reviewRecords[docId].comments = comments; | ||
} | ||
// update review records' comments | ||
comments.push({ | ||
id: t, | ||
op: {p,c,t}, | ||
metadata: { | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
user_id: userId, ts: new Date().toISOString(), | ||
} | ||
}); | ||
// update review threads with `doc_id` | ||
const thread = this.reviewThreads[t]; | ||
if (thread) { | ||
// case 2: `new-comment` arrives first | ||
if (thread.doc_id === undefined) { | ||
thread.doc_id = docId; | ||
this.refreshReviewDecorations(docId); | ||
} | ||
} else { | ||
// case 1: `otUpdateApplied` arrives first | ||
this.reviewThreads[t] = { | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
doc_id: docId, | ||
messages: [], | ||
}; | ||
} | ||
} | ||
// update review records' changes | ||
if (update?.meta?.tc !== undefined) { | ||
const docId = update.doc; | ||
const userId = update.meta?.user_id || ''; | ||
// create new changes array if not exists | ||
if (this.reviewRecords[docId] === undefined) { | ||
this.reviewRecords[docId] = {comments: [], changes: []}; | ||
} | ||
let changes = this.reviewRecords[docId]?.changes; | ||
if (changes === undefined) { | ||
changes = []; | ||
this.reviewRecords[docId].changes = changes; | ||
} | ||
// update review records' changes | ||
for (const op of update.op) { | ||
changes.push({ | ||
id: update.meta.tc, | ||
op, | ||
metadata: { | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
user_id: userId, ts: new Date().toISOString(), | ||
} | ||
}); | ||
} | ||
// debounce refresh review decorations | ||
setTimeout(() => { | ||
this.refreshReviewDecorations(docId); | ||
}, 100); | ||
} | ||
}, | ||
}); | ||
} | ||
|
||
private async refreshReviewDecorations(docId?: string) { | ||
let editors:[vscode.TextEditor, string][] = []; | ||
for (const editor of vscode.window.visibleTextEditors) { | ||
const {fileType, fileId} = await this.vfs._resolveUri(editor.document.uri)!; | ||
if (fileType === 'doc' && (docId === undefined || fileId === docId)) { | ||
editors.push([editor, fileId!]); | ||
} | ||
} | ||
|
||
for (const [editor,docId] of editors) { | ||
// clear previous decorations | ||
Object.values(this.reviewDecorationTypes).forEach((decoration) => { | ||
editor.setDecorations(decoration, []); | ||
}); | ||
// create new decorations for comments | ||
const openRanges = [], resolvedRanges = []; | ||
for (const comment of this.reviewRecords[docId]?.comments || []) { | ||
const thread = comment.thread!; | ||
const range = offsetToRange(editor.document, comment.op.p, comment.op.c); | ||
const hoverMessage = genThreadMarkdownString(thread); | ||
if (thread.resolved) { | ||
resolvedRanges.push({range, hoverMessage}); | ||
} else { | ||
openRanges.push({range, hoverMessage}); | ||
} | ||
} | ||
editor.setDecorations(this.reviewDecorationTypes.openComment, openRanges); | ||
editor.setDecorations(this.reviewDecorationTypes.resolvedComment, resolvedRanges); | ||
// create new decorations for changes | ||
const insertRanges = [], deleteRanges = []; | ||
} | ||
} | ||
|
||
get triggers() { | ||
return []; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters