在之前的文章《ChatGPT 加持下的 Code Review 探索》中,介绍了咱们团队内使用了VS Code来进行code review。为什么开发这样一款插件,包老师的文章也介绍了下面两点原因
Reviewer 需要切换到 Web 浏览器中进行 Review,而 Web 浏览器中的代码展示风格、样式等等和自己的 IDE 中的偏好设置并不一样,这会影响 Reviewer 看代码的效率。
如果 Reviewer 觉得某段代码可以优化,但自己也没有把握该如何优化,自己提供的代码也需要测试验证,所以他一般会需要这么操作:把代码拷贝到自己所使用的 IDE 中,优化完代码后,再粘贴到 Merge Request 中的评论中,这个过程是比较繁琐的。不管如何,编码这个环节肯定是在自己最熟悉的 IDE 中进行才是最高效的,比如还可以使用 Github Copilot 插件对变更代码进行优化。
本节我将介绍这个插件的具体实现。
先来看插件的整体布局
需要实现的功能主要如下
- 账号登录
- 分类MR列表展示
- 点击左侧MR中的变更文件能够在右侧editor区域展示对应文件更改对比、评论操作
- 在下方区域中显示对应修改的ChatGPT分析报告
- reviewer收到review通知后能够快速打开vscode进行reviewer
相信不少同学使用过GitHub Pull Requests
和Gitlab workflow
插件,这两个插件都能够显示MR的变更,刚开始我们也想用这个插件,经过调研,结果很遗憾,这两个插件都不适用于我们团队
- 账号只能登录登录官方网站的账号,我们公司的是内部私有的仓库
- 分类展示也只能显示当前打开项目的MR,我们review同事的代码不可能说需要打开指定的项目吧,这样review也太麻烦了,不符合我们提效的目标
- 无法做到定制内容,例如:显示区域内容,我们需要显示ChatGPT分析报告(分析报告存放于另一个系统)、链接打开mr变更、copilot分析等
基于如上述求,那就我们自己写一个符合团队需求的插件,下面我将介介绍插件的部分功能的实现。
可能部分同学没开发过vscode插件,这没关系,如果你是前端同学,入门也相当的容易,推荐查看
插件基本配置
首先我们要在activeBar
中声明一个应用容器ysfWorkbench
,然后给ysfWorkbench
中绑定一个view
(mergeRequest
)
package.json
...
+ "contributes": {
+ "viewsContainers": {
+ "activitybar": [
+ {
+ "id": "ysfWorkbench", // 这里是容器id,后面绑定视图需要使用
+ "title": "Ysf Workbench",
+ "icon": "assets/images/ys.png"
+ }
+ ]
+ },
+"views": {
+ "ysfWorkbench": [ // 将view绑定到上面的容易中
+ {
+ "id": "mergeRequest", // 在后面view显示的内容会用到
+ "name": "Ysf Workbench",
+ "default": true
+ }
+ ],
+ }
...
登录
在拉取MR数据之前,我们需要登录账号,vscode
贴心的为我们提供了未登录的占位内容,用于显示登录内容,只需要在package.json
中加入viewsWelcome
, 当vscode
context
中ysf.gitlab:noAccount
不存在时显示占位内容
package.json
...
contributes: {
+ "viewsWelcome": [
+ {
+ "view": "mergeRequest",
+ "contents": "欢迎使用vscode插件code review, 由于您还未登录gitlab账号,请先登录\n [立即登录](command:ysf.gitlab:authenticate)",
+ "when": "ysf.gitlab:noAccount" // 读取`context`中的`ysf.gitlab:noAccount`值
+ }
+ ],
}
...
此时,用户点击立即登录
按钮,即可执行ysf.gitlab:authenticate
命令,登录gitlab账号
vscode.commands.registerCommand('ysf.gitlab:authenticate', login)),
唤起登录
登录采用gitlab OAuth进行认证,首先我们需要申请一个Applications
,具体操作可可查看
在login
函数中拼接好url
,然后通过window.open打开链接。格式大概如下
extension.ts
function login() {
const REDIRECT_URI = 'vscode://ysf.ysf-vsc-plugin/authentication'
const APP_ID = '803a04de757168037e1e6c87q3cb4b3ea881e4b52bde9fa1819b3b6d02c68b4d'
window.open(`https://gitlab.example.com/oauth/authorize?client_id=${APP_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&state=STATE&scope=${REQUESTED_SCOPES}&code_challenge=${CODE_CHALLENGE}&code_challenge_method=S256`)
}
随后打开浏览器跳转到gitlab
进行授权,授权完成后会自动打开vscode
(就是浏览器会根据打开链接REDIRECT_URI
),此时gitlab
会拼接上授权code={xxx}
打开vscode,格式大概如下
window.location.href='vscode://ysf.ysf-vsc-plugin/authentication?code=311f08d9c2f5eb7328502764c6a055ade9758c3432403e0a895694ea7a78e372'
vscode 验证登录
浏览器在打开浏览器的时候,会带上路径/authentication
,跟我们的路由是不是很像,那我们怎么取拿到这个路径信息呢?vscode
也很贴心的为我们提供了registerUriHandler
这个方法进行监听
我们只需要稍微加点代码
// 实现一个UriHandle实例
class GitLabUriHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler {
async handleUri(uri: vscode.Uri): Promise<void> {
this.fire(uri);
}
}
const gitlabUriHandler = new GitLabUriHandler();
// 注册uri监听
vscode.window.registerUriHandler(gitlabUriHandler);
// 注册监听回调,当打开vscode时,会自执行gitlabUriHandler.handleUri()
gitlabUriHandler.event((uri) => {
const { path, query, code } = uri;
if (path === '/authentication') { // 这个路径说明是登录
// 在这里进行登录验证,通过code,调用github登录接口
// 具体可查看https://docs.gitlab.com/ee/api/oauth2.html
// 请求api https://gitlab.example.com/oauth/token 验证登录
// .......此处省略888行代码.....
const accessToken = //.... 通过code换取的凭证
if (accessToken) {
// 设置插件上下文,表示已登录账号,这个时候 `mergeRequest`会显示成 treeView视图
// 执行setContext会刷新插件视图,类似于react中的setState,显示不同的内容
vscode.commands.executeCommand('setContext', 'ysf.gitlab:noAccount', false);
}
}
})
MR分类列表
注册treeView
接下来我们需要在extension.ts
中注册一个treeView
(什么是treeView),
extension.ts
// 注册树形视图
vscode.window.registerTreeDataProvider('mergeRequest', mergeRequestProvider);
在mergeRequestProvider
中只需要实现TreeDataProvider中的方法
class MergeRequestProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
// 主要实现下面两个方法
getChildren(element) { // 用户获取子元素
// 这里编写获取子集的代码
// 这里可参考官方的例子
return [
// 例如 new ChangeFile(mr) // mr信息
]
}
getTreeItem(element) { // 用于获取树形视图中的元素的图标、文本和其他显示属性。这个方法负责返回树形视图中各个元素的显示信息
return element // 这里可做更改
}
}
const mergeRequestProvider = new MergeRequestProvider():
class Item extends vscode.TreeItem {
constructor(label) {
super(label) // 还可以传入其他属性,tooltip,description等
}
iconPath = { //标题前的图标,
dark: // 深色图标路径
light: // 亮色图标路径
}
}
export default mergeRequestProvider; // 导出treeData实例
获取MR列表、MR文件变更列表
在上面的代码中,我们注册了显示的treeData
的provider
,我们可以在getChildren
中编写异步代码,比如我们请求分支列表数据
和分支的变更文件数据
,下面是获取变更文件的例子
接口
// 假设已经在接口获取了MR的数据,我们就从接口中拉取变更文件数据
class ChangeFile {
constructor(mr) {
this.mr = mr; // mr信息,包行 mr_iid, project_id
}
async getChildren(element) { // 获取files
const {changes} = fetchMrDetail(mr) // 通过mr_iid, project_id获取数据, https://docs.gitlab.com/ee/api/merge_requests.html#get-single-merge-request-changes
// diffs.map(diff => {
return FileItemModal(changes); // cha
})
....
}
changes 有如下属性
[{
"old_path": "VERSION",
"new_path": "VERSION",
"a_mode": "100644",
"b_mode": "100644",
"diff": "@@ -1 +1 @@\ -1.9.7\ +1.9.8",
"new_file": false,
"renamed_file": false,
"deleted_file": false
}]
点击文件显示变更
当点击最后一级文件时,我们要打开一个diff
的窗口,vscode提供了内置命令vscode.diff
,我们在最后一级绑定一个command
,
vscode.diff
需要传入三个参数
1.老文件的uri
2.新文件的uri
3.title
代码如下
export class ChangedFileModal extends vscode.TreeItem {
constructor(options: { change, mr, mrVersion}) { // mrVersion
const { change, mr, mrVersion } = options;
super(vscode.Uri.file(file.new_path));
this.description = file.new_path;
const uris = getBaseAndHeadUri(mr, mrVersion, file); // 构建vscode需要的uri,这里会加入query信息,包括mr_iid, project_id、commitId等信息,方便在后续用到
this.headFileUri = uris.headFileUri;
this.baseFileUri = uris.baseFileUri;
this.command = { // 绑定命令
title: 'Show changes',
command: VS_COMMANDS.DIFF, // 命令为`vscode.diff`
arguments: [
this.baseFileUri,
this.headFileUri,
`${path.basename(change.new_path)} (!${mr.iid})`,
],
};
}
}
注册文件系统
在你点击文件名想查看对饮的文调用文件对比命令时,此时你会发现vscode报错了
这是因为vscode
默认是读取本地文件,然而我们本地没有这些文件,vscode没有提供通过远程文件读取文件内容的接口,但是为我们提供了自定义文件系统的provider:registerFileSystemProvider
// 注册文件review
vscode.workspace.registerFileSystemProvider(
REVIEW_URI_SCHEME, // 协议名称,自定义,文件系统标识
new ReviewFileSystem(),
ReviewFileSystem.OPTIONS, // 传入为非只读模式
);
// 官方提供的ReadOnlyFileSystem,需要实现readFile来获取文件内容、stat获取文件信息
class ReviewFileSystem extends ReadOnlyFileSystem {
static OPTIONS: RegisterOptions = {
isReadonly: false, // 是否只读,这个在我们调用copilot有用
};
// 获取文件
// 如果是老变更文件这传入是vscode.diff的第一个参数,即arguments[2]
// 如果是新变更文件这传入是vscode.diff的第二个参数,即arguments[1]
async readFile(uri: vscode.Uri): Promise<Uint8Array> {
const params = fromReviewUri(uri); // 这里会获取一些query信息,例如r_iid, project_id、commitId等
if (isEmptyFileUri(uri)) return new Uint8Array();
return this.#readFileRemote(params); // 获取远程文件内容,需要转换成Buffer Uint8Array
}
async stat(uri: vscode.Uri): Promise<vscode.FileStat> {
let size: number;
const params = fromReviewUri(uri);
if (isEmptyFileUri(uri)) {
size = 0;
} else {
size = await this.#sizeRemote(params); // 这里也会调用this.#readFileRemote获取文件打大小
}
return {
type: vscode.FileType.File,
ctime: Date.now(),
mtime: Date.now(),
size,
};
}
}
至此,文件内容已经可以显示在editor中,注意这里会调用4次获取文件内容,有两次获取原变更的内容和大小,有两次获取新变更的内容和大小
通过popo快速打开MR进行review
在开发上面的功能登录认证中我们通过监听/authentication
来完成用户登录验证,给我一个思考:我们已经有popo
通知,能不能通过popo
通知来唤起vscode
来进行review
,这样就不用来查找自己需要review的MR?
经过调研,虽然我们无法通过popo
直接打开vscode,但是我们可以通过http链接打开浏览器,利用浏览器这个中间人来打开vscode
于是在通知中加上了如下信息
vscode查看:https://gate.netease.com/workbench/api/workbench/public/vsc?projectId=110576&mrIid=161
访问这个链接后会用过js打开vscode
<script>
window.location.href = 'vscode://ysf.ysf-vsc-plugin/mr?projectId=110576&mrIid=161';
</script>
显示popo进入的MR视图,然后在popoEnterMrProvider
中实现获取MR的信息,更分类获取MR信息处理一样
gitlabUriHandler.event((uri) => {
const { path, query, code } = uri;
if (path === '/authentication') { // 这个路径说明是登录
...
}
+ if (path === 'mr') {
+ // 这里我们在显示MR列表中再加一个view,当`path === 'mr'`时显示这个view,同时我们在注册一个`treeDataProvider:popoEnterMrProvider`
+ // 存储地址栏的参数信息
+ // 执行setContext会刷新插件视图,类似于react中的setState,显示不同的内容
+ // 同时可以做些优化:比如选中第一个文件
+ vscode.commands.executeCommand('setContext', 'ysf.mr.popo.enter', true);
+ }
})
在package.json中声明一个view,
...
views": {
"ysfWorkbench": [ // 将view绑定到上面的容易中
{
"id": "mergeRequest", // 在后面view显示的内容会用到
"name": "Ysf Workbench",
"default": true
},
+ {
+ "id": "popoEnterMR",
+ "name": "POPO链接进入的MR",
+ "default": true
+ "when": "ysf.mr.popo.enter && !ysf.gitlab:noAccount", // 当ysf.mr.popo.enter存在时会显示改view
+ }
],
}
...
效果如下
显示ChatGPT分析报告
由于我们的web系统中已经存放了分析报告,所以在vscode只要能展示出来即可,这里使用webview显示在editor
下方是个不错的选择
在下方面板中开辟一个tab
出来用于显示我们的报告
"contributes": {
"viewsContainers": {
"activitybar": [
...
],
"panel": [
{
"id": "ysfReportPanel", // 这里声明一个面板
"title": "ChatGPT分析报告",
"icon": "assets/images/ys.png"
}
]
},
}
在extension.ts
中注册webview
,显示咱们报告信息,当文件切换时通知webview页面刷新报告
vscode.window.registerWebviewViewProvider(ysfReportPanel, {
resolveWebviewView: (webviewView) => {
webviewView.webview.options = {...}; // 一些配置
webviewView.title = 'ChatGPT分析报告';
webviewView.webview.html = '....' // 这里是我们的html页面,一般是经过webpack打包后的html文件的内容
webviewView.webview.onDidReceiveMessage((message) => {
// 这里主要是和webview页面进行通信
// 例如当查看的文件变化时,通知页面重新请求对应的报告
// 注册当活动文件变化时的回调函数, 通知webview文件发生变化
const editorDisposable = vscode.window.onDidChangeActiveTextEditor(async (editor) => {
if (editor) {
// 获取当前文件的 URI
const fileUri = editor.document.uri;
if (fileUri.scheme === REVIEW_URI_SCHEME) {
webviewView.webview.postMessage({
command: messageCommand.REPORT_FILE_CHANGED, // 通知文件改变了,在webview页面中通过 window.addEventListener('message', callback)
data: fromReviewUri(fileUri),
});
}
}
});
})
}
})
效果如下图
copilot功能增强
使用copilot帮助阅读代码
前文提到,我们在系统默认的文件对比视图中是无法使用copilot右键功能的,这是因为在copilot源码中禁用了相关功能,当识别到只读模式后是无法显示的
那我们自定义的文件系统就排上用场了,只需要将
isReadonly
传入false
,我们的copilot就可以正常使用
vscode.workspace.registerFileSystemProvider(
REVIEW_URI_SCHEME, // 协议名称,自定义,文件系统标识
new ReviewFileSystem(),
{isReadonly : false}, // 传入为非只读模式
);
使用copilot帮助优化代码
当我们review一段代码的时候我们觉得很有优化空间,希望借助copilot快速帮我们想到优化方案,这个时候可能我们会把这部分代码拷贝到chat窗口输入,这其实也是一个高频重复且繁琐的事,有没有快捷的方法呢?
我通过查看插件的代码(copilot插件代码没开源,这里可以直接找到插件的安装目录,然后直接阅读里面的代码,里面是写编译后的代码,但是没关系,我们只需要先格式化代码,还是能看出些大概得)。在我们点击
explain this
在
github copilot
的源码里是调用了一个命令github.copilot.interactiveEditor.explain
,那么我们也可以在自己的插件里添加一个右键菜单。加一个命定来调用这个copilot
的命令,实现如下
vscode.commands.registerCommand(`ysf.copilot.ptimize`, () => {
const editor = vscode.window.activeTextEditor;
if (!editor) {
return;
}
const selection = editor.selection;
const text = editor.document.getText(selection);
// 获取当前文件的后缀名
const languageId = editor.document.languageId;
// 尽可能列举下所有语言
const languageMap: Record<string, string> = {
js: 'js',
ts: 'ts',
... // 用于映射对应的语言,以便copilot识别更精准
};
const language = languageMap[languageId] || 'js';
vscode.commands.executeCommand('github.copilot.interactiveEditor.explain', `@workspace 帮我优化下面这段代码 \n\n \`\`\` ${language} \n${text} \n\`\`\` \n\n`);
})
这个插件还可以进行代码评论,这部分功能如果有感兴趣的同学,我后面再专门关于如何在vscode评论的文章,这里我就不介绍了
以上是我们在vscode review的一些实践,希望能对大家有所启发。