vscode插件开发流程

1,374 阅读3分钟

一、插件开发前准备

1.下载插件 npm install -g yo generator-code

Yeoman是一个用于Web应用程序的脚手架工具,它可以帮助你快速创建新的项目,提供了一种结构化的方式来组织你的代码和文件;

generator-code是Yeoman的一个生成器,专门用于创建和管理Visual Studio Code扩展。你可以使用"Yo Code"来生成新的VS Code扩展的基本结构,然后在此基础上进行开发;

2.创建模版 yo code

image.png

image.png

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手动上传插件,

image.png

不过通常选择命令行来打包发布,vsce publish major(主版本号) | minor(次版本号) | patch(修订号)| 2.0.1(指定版本号),待Manage Extensions中插件的version更新为此次发布的版本时,就算是发布成功啦!