Skip to content

Commit

Permalink
chore(temp): init integration
Browse files Browse the repository at this point in the history
  • Loading branch information
iamhyc committed Feb 16, 2024
1 parent 4d18c27 commit a7b016f
Show file tree
Hide file tree
Showing 7 changed files with 292 additions and 5 deletions.
1 change: 1 addition & 0 deletions resources/icons/gutter-comment-unresolved.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions resources/icons/gutter-edit.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions resources/icons/gutter-pass.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions src/api/extendedBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ export interface DocumentReviewCommentSchema extends DocumentReviewSchema {
}

export interface DocumentRangesSchema {
changes: DocumentReviewChangeSchema[],
comments: DocumentReviewCommentSchema[],
changes?: DocumentReviewChangeSchema[],
comments?: DocumentReviewCommentSchema[],
}

export interface ExtendedResponseSchema extends ResponseSchema {
Expand Down Expand Up @@ -75,7 +75,7 @@ export class ExtendedBaseAPI extends BaseAPI {
async getAllCommentThreads(identity: Identity, project_id: string) {
this.setIdentity(identity);
return await this.request('GET', `project/${project_id}/threads`, undefined, (res) => {
const threads = JSON.parse(res!);
const threads = JSON.parse(res!);
return {threads};
}) as ExtendedResponseSchema;
}
Expand Down
5 changes: 5 additions & 0 deletions src/collaboration/clientManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SocketIOAPI, UpdateUserSchema } from '../api/socketio';
import { VirtualFileSystem } from '../core/remoteFileSystemProvider';
import { ChatViewProvider } from './chatViewProvider';
import { LocalReplicaSCMProvider } from '../scm/localReplicaSCM';
import { ReviewPanelProvider } from './reviewPanelProvider';

interface ExtendedUpdateUserSchema extends UpdateUserSchema {
selection?: {
Expand Down Expand Up @@ -59,6 +60,7 @@ export class ClientManager {
private readonly onlineUsers: {[K:string]:ExtendedUpdateUserSchema} = {};
private connectedFlag: boolean = true;
private readonly chatViewer: ChatViewProvider;
private readonly reviewPanel: ReviewPanelProvider;

constructor(
private readonly vfs: VirtualFileSystem,
Expand Down Expand Up @@ -101,6 +103,7 @@ export class ClientManager {
});

this.chatViewer = new ChatViewProvider(this.vfs, this.publicId, this.context.extensionUri, this.socket);
this.reviewPanel = new ReviewPanelProvider(this.vfs, this.context, this.socket);
this.status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 0);
this.updateStatus();
}
Expand Down Expand Up @@ -376,6 +379,8 @@ export class ClientManager {
}),
// register chat view provider
...this.chatViewer.triggers,
// register review panel provider
...this.reviewPanel.triggers,
// update this client's position
vscode.window.onDidChangeTextEditorSelection(async e => {
if (e.kind===undefined) { return; }
Expand Down
279 changes: 279 additions & 0 deletions src/collaboration/reviewPanelProvider.ts
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 [];
}
}
4 changes: 2 additions & 2 deletions src/core/remoteFileSystemProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1187,10 +1187,10 @@ export class VirtualFileSystem extends vscode.Disposable {
const threadsRes = await (this.api as ExtendedBaseAPI).getAllCommentThreads(identity, this.projectId);
if (threadsRes.type==='success') {
for (const [docId, range] of Object.entries(rangesRes.ranges)) {
for (const comment of range.comments) {
range.comments?.forEach((comment) => {
comment.thread = threadsRes.threads[comment.op.t];
threadsRes.threads[comment.op.t].doc_id = docId;
}
});
}
const [ranges, threads] = [rangesRes.ranges, threadsRes.threads];
return {ranges, threads};
Expand Down

0 comments on commit a7b016f

Please sign in to comment.