日志分析工具-vscode插件

这次来看一下如何开发vscode插件,插件的功能是方便排查本地日志文件。

vscode文档官网地址 : Your First Extension

配置环境

请先安装好Node.js和Git,

然后安装Yeoman和VS Code Extension Generator:

1
2
3
# 安装脚手架
# node的版本最好升到16以上
npm install -g yo generator-code

通过脚手架生成模板项目,我们可以通过这个模板工程来开发:

1
2
# 使用 yo 生成模板工程
yo code

输入yo code后会出现一个命令行交互:进行对应的信息输入就好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

_-----_ ╭──────────────────────────╮
| | │ Welcome to the Visual │
|--(o)--| │ Studio Code Extension │
`---------´ │ generator! │
( _´U`_ ) ╰──────────────────────────╯
/___A___\ /
| ~ |
__'.___.'__
´ ` |° ´ Y `

? What type of extension do you want to create? New Extension (JavaScript)
? What's the name of your extension? helloWorld
? What's the identifier of your extension? helloWorld
? What's the description of your extension? helloWorld
? Enable JavaScript type checking in 'jsconfig.json'? Yes
? Initialize a git repository? No
? Which package manager to use? npm

完成交互后 命令行会出现

1
2
3
4
5
Your extension helloWorld has been created!

To start editing with Visual Studio Code, use the following commands:

code helloWorld

调试helloWorld

用VS Code打开helloWorld文件夹 ,按下F5,你会立即看到一个插件发开主机窗口,其中就运行着插件。

在命令面板(Ctrl+Shift+P)中输入Hello World命令。

如果你看到了Hello World提示弹窗,恭喜你成功了!

插件目录结构

1
2
3
4
5
6
7
8
9
10
.
├── .vscode
│ ├── launch.json // 插件加载和调试的配置
│ └── tasks.json // 配置TypeScript编译任务
├── .gitignore // 忽略构建输出和node_modules文件
├── README.md // 一个友好的插件文档
├── src
│ └── extension.ts // 插件源代码
├── package.json // 插件配置清单
├── tsconfig.json // TypeScript配置

目前我们只有着重看package.jsonextension.ts 文件

每个VS Code插件都必须包含一个package.json,它就是插件的配置清单。package.json混合了Node.js字段,如:scripts、dependencies,还加入了一些VS Code独有的字段,如:publisher、activationEvents、contributes等。关于这些VS Code字段说明都在插件清单参考中可以找到。

extension.ts 插件入口文件会导出两个函数,activate 和 deactivate,你注册的激活事件被触发之时执行activate,deactivate则提供了插件关闭前执行清理工作的机会。

所需的插件功能

目前基本了解一下vscode插件的大概,接下来具体看下我们可能需要哪些功能:

  1. 一个左侧pannel上显示的icon
  2. 点击icon打开的左侧面板UI
    • 面板上需要打开文件功能
    • 面板上需要将当前打开的文件设置为所需文件功能
    • 一个输入筛选条件的输入框
    • 根据筛选条件过滤文件内容的功能
    • 将过滤后信息输出的功能

侧边icon:contributes.viewsContainers & contributes.views

在package.json中通过viewsContainers 和 views 配置来声明 左侧pannel上显示的icon 和 点击icon打开的左侧面板UI。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
"contributes": {
"viewsContainers": {
"activitybar": [
{
"id": "ai-log-assistant",
"title": "AI Log Assistant",
"icon": "resources/dark/ai-kit-icon.svg"
}
]
},
"views": {
"ai-log-assistant": [
{
"id": "ai-log-assistant",
"type": "webview",
"name": "AI Log Assistant",
"icon": "resources/dark/dependency.svg",
"contextualTitle": "AI Logger"
}
]
},
}

侧边视图:自定义webview

在 extension.ts 中注册一个 与 package.json 对应的 ai-log-assistan 侧边栏ID

1
2
3
4
5
6
7
8
9
import * as vscode from "vscode";
import { SidebarProvider } from "./SidebarProvider";

export function activate(context: vscode.ExtensionContext) {
const sidebarPanel = new SidebarProvider(context.extensionUri);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider("ai-log-assistant", sidebarPanel)
);
}

实现侧边栏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import * as vscode from "vscode";

export class AILogWebviewViewProvider implements vscode.WebviewViewProvider {
public static readonly viewType = 'ai-log-assistant';

private _view?: vscode.WebviewView;

private readonly _extensionUri: vscode.Uri;
private readonly _context: vscode.ExtensionContext;

constructor(extensionUri: vscode.Uri, context: vscode.ExtensionContext) {
this._extensionUri = extensionUri;
this._context = context;
}

public resolveWebviewView(webviewView: vscode.WebviewView): void {
this._view = webviewView;

this._view.webview.options = {
enableScripts: true,
};
const projectPath = utils.getProjectPath(this._context);
utils.loadToken();
console.log("projectPath: " + projectPath);
this._view.webview.onDidReceiveMessage(message => {
operateMessage(this._context, projectPath, webviewView.webview, message);
}, undefined, this._context.subscriptions);
this._view.webview.html = this._getHtmlForWebview(webviewView);
}

private _getHtmlForWebview(view: vscode.WebviewView): string {
return getWebViewContent(this._context, 'view/ai-webview.html', view.webview);
}
}

上述代码采用面向对象的方式实现一个 AILogWebviewViewProvider类,根据 vscode.WebviewViewProvider,
其实实现所有的 WebviewViewProvider 都是是这段代码,其他代码都是相同的,因为关于 webview 中的 HTML 我们都可以使用js来生成,这不正是我们的单页面应用开发?

UI与逻辑通信

在webview html的按钮点击事件中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 调用vscode原生api
* @param data 可以是类似 {cmd: 'xxx', param1: 'xxx'},也可以直接是 cmd 字符串
* @param cb 可选的回调函数
*/
function callVscode(data, cb) {
if (typeof data === 'string') {
data = { cmd: data };
}
if (cb) {
// 时间戳加上5位随机数
const cbid = Date.now() + '' + Math.round(Math.random() * 100000);
callbacks[cbid] = cb;
data.cbid = cbid;
}
vscode.postMessage(data);
}

在上面AILogWebviewViewProvider注册了:

1
2
3
this._view.webview.onDidReceiveMessage(message => {
operateMessage(this._context, projectPath, webviewView.webview, message);
}, undefined, this._context.subscriptions);

然后通过operateMessage分发对应的时间请求:

1
2
3
4
5
6
function operateMessage(context: vscode.ExtensionContext, projectPath: string, webview: vscode.Webview, message: any) {
console.log("received msg : " + message);
switch (message.cmd) {

}
}

打开文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function selectXLogFile(pcontext: vscode.ExtensionContext, webview: vscode.Webview, message: any) {
// 打开文件选择对话框
const uris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
});

// 如果用户选择了文件,则打开它
if (uris && uris.length > 0) {
const uri = uris[0];
parseFilePath = uri.fsPath;
console.log("select a log file: " + parseFilePath);
invokeCallback(webview, message, parseFilePath);
}
}

获取当前打开的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function parseCurLogFile(pcontext: vscode.ExtensionContext, webview: vscode.Webview, message: any) {
console.log("start parseCurLogFile");
curShowDocument = null;
const editor = vscode.window.activeTextEditor;
let filePath = '';
if (editor) {
// 获取当前打开文件的路径
filePath = editor.document.uri.fsPath;
// 获取当前打开文件的文件名
const fileName = editor.document.fileName;
console.log("filePath: " + filePath + " fileName:" + fileName);
}

if (filePath === '') {
utils.showError("当前文件信息为空");
invokeCallback(webview, message, "当前文件信息为空");
return;
}

if (!filePath.endsWith(".log")) {
utils.showError(filePostfixHint);
}
parseFilePath = filePath;
invokeCallback(webview, message, parseFilePath);
// startFillter(pcontext, webview, message);
}