3

vscode是个超级好用的开发工具,谁用谁知道。

背景故事

一个项目开发、维护的时间久了之后,总会多多少少碰到一段不是你写的,而现在你要维护,但你却看不明白的那是什么鬼的代码;当然有时候也可能是多人在同一个项目里协作开发时,对于bug最终责任人的确诊问题(找到最终责任人不是要“修理他”。帮助他认识问题,提高自身能力,加深团队协作意识才是重点)。

大多数人都用git,也知道git里诸如:git loggit blame等命令都可以帮我们做到以上需求,可每次看到一段代码之后,先从IDE里切换到Terminal下,然后敲打命令,并且要记好事故代码的行号,以及准确的文件路径,敲来敲去还是挺烦人的。

于是,能在不来回切换工作环境的情况下,迅速找到某一段代码的作者、写作时间、写作目的就显的还是有点用了。

一个扩展的诞生

写作思路

为了更直观表达这个扩展的设计思路,我用了一个图:

图片描述

后面我们就根据图里描述的思路来开展工作。

获取信息

要得到一段指定行号(行区间)代码的信息,大家都知道用git blame,展示结果如下:

»git blame -L 10,11 js/index.js

297cb0df (Howard.Zuo 2017-03-08 21:58:03 +0800 10)     render(h) {
297cb0df (Howard.Zuo 2017-03-08 21:58:03 +0800 11)         return h(Game);

少了,"写作目的",也就是说,这个命令的结果无法告诉我们作者提交这段代码时的commit message写了什么。于是我得翻翻git blame --help,可喜的是被我找到了--line-porcelain选项,在她的描述里看到了这么一句:but output commit information for each line。好像有戏,来试试看:

»git blame -L 10,11 js/index.js --line-porcelain

297cb0df8ab1fe06ee935798d1a2dd4e712a070d 10 10 2
author Howard.Zuo
author-mail <leftstick@qq.com>
author-time 1488981483
author-tz +0800
committer Howard.Zuo
committer-mail <leftstick@qq.com>
committer-time 1488981503
committer-tz +0800
summary fix duplicate key issue
previous 43966e5cc4c70998c265781fe8acf02946c28f6e js/index.js
filename js/index.js
        render(h) {
297cb0df8ab1fe06ee935798d1a2dd4e712a070d 11 11
author Howard.Zuo
author-mail <leftstick@qq.com>
author-time 1488981483
author-tz +0800
committer Howard.Zuo
committer-mail <leftstick@qq.com>
committer-time 1488981503
committer-tz +0800
summary fix duplicate key issue
previous 43966e5cc4c70998c265781fe8acf02946c28f6e js/index.js
filename js/index.js
            return h(Game);

不错,这明显是个可以被解析的数据结构。所以获取信息就靠她了git blame -L <start,end> <filePath> --line-porcelain

因为vscode是一个基于electron开发的IDE,所以node.js的API对我们是可用的,于是可以通过如下代码执行上面的命令,并且拿到结果数据:

//vscode api,获取当前正在操作的编辑页面
const editor = vscode.window.activeTextEditor;
if (!editor) {
    vscode.window.showWarningMessage('You have to active a file first');
    return;
}

//拿到鼠标选择的内容
const selection = editor.selection;

//合成git命令
const cmd = `git blame -L ${selection.start.line + 1},${selection.end.line + 1} ${editor.document.fileName} --line-porcelain`;

//通过child_press执行该命令,
child_process.exec(cmd, {
    cwd: vscode.workspace.rootPath
}, (error, stdout, stderr) => {
    //这里的stdout就是上面我们看到的输出内容了
});

解析信息

看了上面的输出内容,我们需要一个model来描述一个这样一个条目:

export interface Item {
    hash: string;
    shortHash: string;
    author: string;
    authorEmail: string;
    authorTime: number;
    authorTz: string;
    committer: string;
    committerEmail: string;
    committerTime: number;
    committerTz: string;
    commitMessage: string;
    previousCommit?: string;
    fileName: string;
    change: string;
}

接下来就是如何解析git命令的输出结果了,一个大大的循环来搞定:

export function parse(output: string): Array<Item> {
    const lines = output.replace(/\r\n/mg, '\n').split('\n');

    const commits: Array<Item> = [];

    let commit;

    for (let i = 0; i < lines.length - 1; i++) {
        const line = lines[i];
        
        //一个item的开始标志就是commit的hash
        if (/^[a-z0-9]{15,}/.test(line)) {
            commit = {};
            commits.push(commit);
            commit.hash = line.split(' ')[0];
            commit.shortHash = commit.hash.substring(0, 8);

        } else if (/^author\s/.test(line)) {
            commit.author = line.split(' ')[1];
        } else if (/^author-mail\s/.test(line)) {
            commit.authorEmail = line.split(' ')[1];
        } else if (/^author-time\s/.test(line)) {
            commit.authorTime = +line.split(' ')[1];
        } else if (/^author-tz\s/.test(line)) {
            commit.authorTz = line.split(' ')[1];
        } else if (/^committer\s/.test(line)) {
            commit.committer = line.split(' ')[1];
        } else if (/^committer-mail\s/.test(line)) {
            commit.committerEmail = line.split(' ')[1];
        } else if (/^committer-time\s/.test(line)) {
            commit.committerTime = +line.split(' ')[1];
        } else if (/^committer-tz\s/.test(line)) {
            commit.committerTz = line.split(' ')[1];
        } else if (/^previous\s/.test(line)) {
            commit.previousCommit = line.split(' ')[1];
        } else if (/^filename\s/.test(line)) {
            commit.fileName = line.split(' ')[1];
        } else if (!commit.fileName) {
            commit.commitMessage = line;
        } else {
            commit.change = line;
        }
    }

    return commits;
}

展示

有了上面解析完毕的Array<Item>,简单转换一下字符串,对任何人都是没什么难度的:

export function pretty(commits: Array<Item>): string {
    return commits.map(c => {
        return `${c.shortHash} ${c.author} ${formatDate(c.authorTime * 1000)} "${c.commitMessage}" ${c.change}`;
    })
        .join('\n');
}

解决了所有核心问题,剩下的就是按照vscode-docs,动手补充一个扩展需要的额外因素了。

安装代码骨架生成器

npm install -g yo generator-code

生成插件项目

yo code

在提问中,依次回答所有问题,最后项目骨架生成。

生成右键菜单

修改package.json,增加/修改contributes字段:

"contributes": {
    "menus": {
        "editor/context": [{
            "command": "extension.gitblame",
            "group": "sourcecontrol",
            "when": "config.git.enabled && scmProvider == git && gitState == idle"
        }]
    },
    "commands": [{
        "command": "extension.gitblame",
        "title": "Git blame",
        "key": "ctrl+b"
    }],
    "keybindings": [{
        "command": "extension.gitblame",
        "key": "alt+b"
    }]
}

于是乎,一个右键选项Git blame就会出现在你选择一段代码后的右键菜单里

懒加载扩展

一个扩展根据需求,并不一定要随vscode的启动而启动,那样影响vscode的整体性能,而且没什么特别的好处。所以我们要懒加载。修改package.json,增加/修改activationEvents字段:

"activationEvents": [
    "onCommand:extension.gitblame"
]

只有当用户使用extension.gitblame这个命令时(也就是在右键菜单里选择了Git blame时),该扩展才正式激活。

到此为止,这个扩展就基本完成了。效果如下:

图片描述

扩展下载地址:vscode-git-blamer

项目源码地址:vscode-git-blamer


leftstick
27.3k 声望1.5k 粉丝

沙滩一卧两年半,今日浪打我翻身