Skip to content

Commit

Permalink
Feature/code review (#817)
Browse files Browse the repository at this point in the history
* detect when a document is opened for diff

* fix how we decide a diff editor is open, and show some dummy comment

* make addComments async + prettier

* support returning multiple threads when commenting

* invoke real api to get comments

* use diff to query for changes only on changed file

* support removing a code review comment

* lint + prettier

* Add status bar message when fetching suggestions

* prettier

* downgrade axios to fix build

* fix tabnine icon in comments

* cleanup

* display formatted code in suggestions

* lint + prettier

* support applying a suggestion

* cleanup

* fire events

* not needed

* do not sent code snippets

* do not do anythong if CODE_REVIEW capability is not enabled

* lint + prettier

* pr comments

* remove unneeded

* lint + prettier
  • Loading branch information
amircodota authored Apr 26, 2022
1 parent 1505f93 commit 0f39ccf
Show file tree
Hide file tree
Showing 14 changed files with 467 additions and 3 deletions.
10 changes: 10 additions & 0 deletions assets/approve.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions assets/approve_inverse.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 assets/close.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 assets/close_inverse.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 35 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
"devDependencies": {
"@types/chai": "^4.2.14",
"@types/debounce": "^1.2.1",
"@types/diff": "^5.0.2",
"@types/glob": "^7.1.3",
"@types/mocha": "^8.2.2",
"@types/mock-fs": "^4.13.0",
Expand Down Expand Up @@ -165,7 +166,9 @@
},
"dependencies": {
"await-semaphore": "^0.1.3",
"axios": "^0.21.0",
"debounce": "^1.2.1",
"diff": "^5.0.0",
"extract-zip": "^2.0.1",
"https-proxy-agent": "^5.0.0",
"semver": "^7.3.2",
Expand Down Expand Up @@ -230,7 +233,24 @@
{
"command": "Tabnine.saveSnippet",
"title": "⌬ Tabnine: Save Snippet"
}
},
{
"command": "Tabnine.hideSuggestion",
"title": "Hide",
"icon": {
"dark": "assets/close_inverse.svg",
"light": "assets/close.svg"
}
},
{
"command": "Tabnine.applySuggestion",
"title": "Apply",
"icon": {
"dark": "assets/approve_inverse.svg",
"light": "assets/approve.svg"
}
}

],
"menus": {
"editor/context": [
Expand All @@ -252,7 +272,20 @@
"command": "TabNine::assistantToggle",
"when": "tabnine-assistant:capability"
}
]
],
"comments/commentThread/title": [
{
"command": "Tabnine.hideSuggestion",
"group": "navigation",
"when": "commentController == tabnine.commentController"
},
{
"command": "Tabnine.applySuggestion",
"group": "navigation",
"when": "commentController == tabnine.commentController"
}

]
},
"configuration": [
{
Expand Down
1 change: 1 addition & 0 deletions src/capabilities/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export enum Capability {
AUTHENTICATION = "vscode.authentication",
NOTIFICATIONS_WIDGET = "vscode.notifications-widget",
TABNINE_TODAY_WIDGET = "vscode.tabnine-today-widget",
CODE_REVIEW = "vscode.code-review",
SAVE_SNIPPETS = "save_snippets",
BETA_CAPABILITY = "beta",
}
Expand Down
16 changes: 16 additions & 0 deletions src/codeReview/DocumentThreads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { CommentThread, Uri } from "vscode";

export default class DocumentThreads {
readonly uri: Uri;

private readonly threads: CommentThread[];

constructor(uri: Uri, threads: CommentThread[]) {
this.uri = uri;
this.threads = threads;
}

dispose(): void {
this.threads.forEach((thread) => thread.dispose());
}
}
101 changes: 101 additions & 0 deletions src/codeReview/TabnineComment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {
CommentMode,
CommentThread,
Uri,
MarkdownString,
Comment,
CommentAuthorInformation,
workspace,
WorkspaceEdit,
TextDocument,
} from "vscode";
import { fireEvent } from "../binary/requests/requests";
import * as api from "./api";

export default class TabnineComment implements Comment {
suggestion: api.Suggestion;

language: string;

oldValue: string;

constructor(oldValue: string, suggestion: api.Suggestion, language: string) {
this.oldValue = oldValue;
this.suggestion = suggestion;
this.language = language;
}

get body(): MarkdownString {
return new MarkdownString().appendCodeblock(
this.suggestion.value,
this.language
);
}

// eslint-disable-next-line class-methods-use-this
get mode(): CommentMode {
return CommentMode.Preview;
}

// eslint-disable-next-line class-methods-use-this
get author(): CommentAuthorInformation {
const iconUri = Uri.parse(
"https://www.tabnine.com/favicons/favicon-32x32.png"
);
return { name: "Tabnine", iconPath: iconUri };
}

apply(thread: CommentThread): boolean {
const document = documentOf(thread);

if (!document) {
this.fireEvent("comment-applied", thread, {
success: false,
failureReason: "doument not found",
});
return false;
}

const oldText = document.getText(thread.range);

if (this.oldValue !== oldText.trim()) {
this.fireEvent("comment-applied", thread, {
success: false,
failureReason: "text did not match",
});
return false;
}

const edit = new WorkspaceEdit();
edit.replace(thread.uri, thread.range, this.suggestion.value);
void workspace.applyEdit(edit);

this.fireEvent("comment-applied", thread, { success: true });

return true;
}

hide(thread: CommentThread): void {
this.fireEvent("comment-hidden", thread);
thread.dispose();
}

// eslint-disable-next-line class-methods-use-this
private fireEvent(
event: string,
thread: CommentThread,
additionalProperties: Record<string, unknown> = {}
): void {
void fireEvent({
name: `code-review-${event}`,
lineIndex: thread.range.start.line,
file: thread.uri.path,
lineCount: documentOf(thread)?.lineCount,
...additionalProperties,
});
}
}

function documentOf(thread: CommentThread): TextDocument | undefined {
return workspace.textDocuments.find((doc) => doc.uri === thread.uri);
}
50 changes: 50 additions & 0 deletions src/codeReview/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import axios from "axios";
import tabnineExtensionProperties from "../globals/tabnineExtensionProperties";

const instance = axios.create({
baseURL: tabnineExtensionProperties.codeReviewBaseUrl,
timeout: 30000,
});

interface ExtensionsResponse {
extensions: string[];
}

export interface Range {
start: number;
end: number;
}

export interface SuggestionsRequest {
filename: string;
buffer: string;
ranges: Range[];
threshold: string;
}

export interface Suggestion {
value: string;
classification: { type: string; description: string };
}

export interface Suggestions {
start: number;
old_value: string;
suggestions: Suggestion[];
}

export interface SuggestionsResponse {
filename: string;
focus: Suggestions[];
}

export async function supportedExtensions(): Promise<ExtensionsResponse> {
return (await instance.get<ExtensionsResponse>("languages/extensions")).data;
}

export async function querySuggestions(
payload: SuggestionsRequest
): Promise<SuggestionsResponse> {
return (await instance.post<SuggestionsResponse>("suggestions", payload))
.data;
}
92 changes: 92 additions & 0 deletions src/codeReview/codeReview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as vscode from "vscode";
import { Capability, isCapabilityEnabled } from "../capabilities/capabilities";
import addSuggestions from "./suggestions";
import DocumentThreads from "./DocumentThreads";
import TabnineComment from "./TabnineComment";

let activeThreads: DocumentThreads | null = null;

export default function registerCodeReview(): void {
const controller = vscode.comments.createCommentController(
"tabnine.commentController",
""
);
controller.options = {
placeHolder: "",
prompt: "",
};

vscode.commands.registerCommand(
"Tabnine.hideSuggestion",
(thread: vscode.CommentThread) => {
const comment = thread.comments[0] as TabnineComment | undefined;

if (comment) {
comment.hide(thread);
}
}
);

vscode.commands.registerCommand(
"Tabnine.applySuggestion",
(thread: vscode.CommentThread) => {
const comment = thread.comments[0] as TabnineComment | undefined;

if (comment && comment.apply(thread)) {
thread.dispose();
}
}
);

vscode.window.onDidChangeActiveTextEditor(async () => {
if (!isCapabilityEnabled(Capability.CODE_REVIEW)) return;

const diffEditor = getActiveDiffEditor();

let newThreads = null;
if (diffEditor) {
newThreads = await addSuggestions(
controller,
diffEditor.newEditor.document,
diffEditor.oldEditor.document
);
}

if (activeThreads) {
activeThreads.dispose();
}

activeThreads = newThreads;
});
}

function getActiveDiffEditor(): {
oldEditor: vscode.TextEditor;
newEditor: vscode.TextEditor;
} | null {
const visibleEditors = vscode.window.visibleTextEditors;
if (visibleEditors.length !== 2) return null;
if (
visibleEditors[0].document.uri.path !== visibleEditors[1].document.uri.path
)
return null;

const oldEditor = visibleEditors.find(isHeadGitEditor);
const newEditor = visibleEditors.find(
(editor) => editor.document.uri.scheme === "file"
);

if (oldEditor && newEditor) {
return { oldEditor, newEditor };
}
return null;
}

function isHeadGitEditor(editor: vscode.TextEditor): boolean {
if (editor.document.uri.scheme === "git") {
const query = JSON.parse(editor.document.uri.query) as { ref: string };
return query.ref === "HEAD" || query.ref === "~";
}

return false;
}
Loading

0 comments on commit 0f39ccf

Please sign in to comment.