一、插件开发前准备
1.下载插件 npm install -g yo generator-code
Yeoman是一个用于Web应用程序的脚手架工具,它可以帮助你快速创建新的项目,提供了一种结构化的方式来组织你的代码和文件;
generator-code是Yeoman的一个生成器,专门用于创建和管理Visual Studio Code扩展。你可以使用"Yo Code"来生成新的VS Code扩展的基本结构,然后在此基础上进行开发;
2.创建模版 yo code
3.目录结构:
.vscode/extensions.json 为项目的开发者推荐一些安装的插件
.vscode/launch.json 关于开发调试和测试的配置
.vscode/settings.json 关于项目工作区的配置
.vscode/tasks.json 关于项目命令行任务的配置
extension.ts 主要编写内容的文件
.vscodeignore 忽略打包插件的文件(使用vsce package命令,打包为.vsix 文件,可直接在vscode中安装)
CHANGELOG.md 发布版本的更新内容说明
README.md 插件简介
vsc-extension-quickstart.md 新手开发指南
二、插件开发
import * as vscode from 'vscode';
import SelectionViewProvider from './containers/selection-view-provider';
import ChatViewProvider from './containers/chat-view-provider';
import { pathToParams } from './utils/index';
import myHttp from './utils/http';
export function activate(context: vscode.ExtensionContext) {
console.log('gdcodegpt插件已激活');
// 选中展示webview注册
const selectionProvider = new SelectionViewProvider(context.extensionUri);
const selctionDisposable = vscode.window.registerWebviewViewProvider(
// package.json中views配置下,对应渲染的webview的唯一标识
SelectionViewProvider.viewId,
selectionProvider,
// 当切换其他插件界面时,不清除该插件的webview页面节点
{ webviewOptions: { retainContextWhenHidden: true } }
);
// 交互窗口webview注册
const chatProvider = new ChatViewProvider(context.extensionUri, context);
const chatDisposable = vscode.window.registerWebviewViewProvider(
ChatViewProvider.viewId,
chatProvider,
{ webviewOptions: { retainContextWhenHidden: true } }
);
// 监听授权成功后的schema回调
vscode.window.registerUriHandler({
handleUri(uri: vscode.Uri): vscode.ProviderResult<void> {
// console.log(uri, uri.query);
if (uri.scheme !== 'vscode') return;
const params = pathToParams(uri.query);
const { accessToken, refreshToken } = params;
if (!accessToken) return;
vscode.window.showInformationMessage('授权成功~');
chatProvider.saveTokenInfo({ accessToken, refreshToken });
}
});
// 编辑区插入内容命令注册
const editorInsertCommand = vscode.commands.registerCommand(
'editor.continuous.insert',
async () => {
if (myHttp.accessToken === '') {
chatProvider.locationAuthPage();
return;
}
if (chatProvider.editorRes) return;
// 展示输入弹窗,获取输入内容
const description = await vscode.window.showInputBox({
ignoreFocusOut: true,
placeHolder: '请输入你的描述',
prompt: '尽可能详细描述你的诉求,便于GDCodeGPT给予更专业的回复'
});
if (!description) return;
chatProvider.insertingContentArr = [];
chatProvider.skip = false;
chatProvider.pause = false;
chatProvider.changeStatusBar(false, 'continue');
chatProvider.getChatGPTResult(description, 'editor', 'code');
}
);
// 注册内容挂载上下文
context.subscriptions.push(
selctionDisposable,
chatDisposable,
editorInsertCommand
);
}
export function deactivate(){
console.log('gdcoegpt插件已卸载');
}
{
/* `${name}.${publisher}`组合为插件id,用于发布插件市场,不可重复 */
"name": "gdcodegpt",
"publisher": "gaodun",
/* 插件名称 */
"displayName": "GDCodeGPT",
/* 插件logo */
"icon": "resources/logo.png",
/* 插件描述 */
"description": "基于高顿gdbot平台,实现知识查询、代码修复、代码优化、代码注释、代码生成等功能",
/* 插件版本 */
"version": "1.1.1",
/* 代码仓库 */
"repository": "https://gitlab.gaodun.com/static/vscodechatgpt",
"engines": {
/* 插件安装所需vscode的版本 */
"vscode": "^1.70.0"
},
/* 插件类型,便于插件被用户搜索 */
"categories": [
"Programming Languages",
"Other"
],
/* 激活事件 */
"activationEvents": ["onLanguage:javascript"],
/* 主文件路径 */
"main": "./dist/extension.js",
"contributes": {
/* 指令定义 */
"commands": [
{
"command": "editor.continuous.insert",
"title": "持续插入内容"
},
{
"command": "editor.continuous.insert.pause",
"title": "暂停持续插入内容"
},
{
"command": "editor.continuous.insert.continue",
"title": "继续持续插入内容"
}
],
/* 快捷键定义 */
"keybindings": [
{
"command": "editor.continuous.insert",
"mac": "cmd+k",
"win": "alt+k"
}
],
"menus": {
/* 在鼠标右击出现的菜单栏中添加指令 */
"editor/context": [
{
"command": "editor.continuous.insert",
"group": "1_modification"
}
]
},
"viewsContainers": {
/* 插件侧边栏设置 */
"activitybar": [
{
"id": "sidebar_gdcodegpt",
"title": "GDCodeGPT",
"icon": "resources/logo.svg"
}
]
},
"views": {
/* 插件侧边栏空间中的webview配置 */
"sidebar_gdcodegpt": [
{
"type": "webview",
"id": "selection.webview",
"name": "选中展示"
},
{
"type": "webview",
"id": "chat.webview",
"name": "交互窗口"
}
]
}
},
...
}
import * as vscode from 'vscode';
import { getSelection, getNonce } from '@/utils';
class SelectionViewProvider implements vscode.WebviewViewProvider {
// webview唯一标识
public static readonly viewId = 'selection.webview';
private _view?: vscode.WebviewView;
constructor(
// 插件运行上下文路径
private readonly _extensionUri: vscode.Uri,
) { }
public resolveWebviewView(
// 视图基础信息
webviewView: vscode.WebviewView
) {
this._view = webviewView;
webviewView.webview.options = {
// 是否允许在Webview中运行脚本
enableScripts: true,
// Webview可以加载的资源的根目录
localResourceRoots: [this._extensionUri],
};
// webview页面的html内容
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
// 编辑器选中内容变化时,发送消息给webview
vscode.window.onDidChangeTextEditorSelection(() => {
this.sendSelection();
});
}
public sendSelection() {
if (!this._view?.visible) return;
const selection = getSelection();
// 向webview页面发送消息
this._view.webview.postMessage(selection);
}
// 获取待渲染节点信息
private _getHtmlForWebview(webview: vscode.Webview) {
// asWebviewUri 将本地文件系统的 uri 转换为可以在 webviews 中使用的 uri
const selectionUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'media', 'selection.js'));
const markeddUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "media", "scripts", "marked.min.js"));
const highlightUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "media", "scripts", "highlight.min.js"));
const styleHighlighUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'media', 'css', 'highligh.css'));
const nonce = getNonce();
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src ${webview.cspSource}; script-src 'nonce-${nonce}'; child-src 'none'">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="${styleHighlighUri}" rel="stylesheet">
<title>gdcodegpt</title>
</head>
<body>
<div class="selection">
</body>
<script nonce="${nonce}" src="${markeddUri}"></script>
<script nonce="${nonce}" src="${highlightUri}"></script>
<script nonce="${nonce}" src="${selectionUri}"></script>
</html>`;
}
}
export default SelectionViewProvider;
三、开发难点
1.企业微信扫码登录重定向的方式无法获取解析AccessToken所需的code
原因:vscode出于安全性和隔离性的考虑,无法在其中使用location.href来进行重定向跳转,所以无法在扫码之后,从重定向的路由上拿到对应的code。
解决方案:通过vscode.env.openExternal打开用户的默认浏览器来访问扫码页面,扫码成功后,使用重定向路由上的code换取accessToken和refreshToken,之后再利用vscode://gaodun.gdcodegpt?accessToken=${}&refreshToken=${}的schema协议跳回到vscode中,利用vscode.window.registerUriHandler完成信息的接收,实现插件的登录操作。
2.使用ChatGPT的流式返回,在编辑区域中逐字插入代码时,出现信息错乱
原因:第一,由于流式返回和编辑区的插入操作都是属于异步行为,所以插入内容时会丢失部分信息;第二,在插入过程中,遇到换行符,没有重置光标到列的开始位置;
解决方案:使用Nodejs的https模块与ChatGPT相应接口建立连接后,在res.on('data',chunk => {...获信息,解析,插入编辑区})中完成信息读写操作,利用res.pause()和res.resume()将读取信息操作变为同步行为,读取一段信息,暂停读流,等待插入代码完成后,再继续读取。
四、插件发布
第一步:下载vsce(命令行工具,用来打包/发布/管理插件的命令行工具),npm install -g vsce
第二步:在Azure DevOps上创建组织,然后创建一个项目,最后创建个人访问令牌
第三步:在Marketplace创建一个publisher(必须使用上一步创建个人访问令牌的微软账号),这里的publisher需要和插件项目中的package.json里的publisher字段的value值一样,在发布时,和我们的插件名组合成插件的ID,即${publisher}.${name}(每个插件的ID都是唯一的)
最后一步:发布插件,可以选择在Manage Extensions手动上传插件,
不过通常选择命令行来打包发布,vsce publish major(主版本号) | minor(次版本号) | patch(修订号)| 2.0.1(指定版本号),待Manage Extensions中插件的version更新为此次发布的版本时,就算是发布成功啦!