背景
最近在研究低代码,发现很多的低代码都是表单生成器。自己也照猫画虎地造了一下轮子,因为一开始的方向就是生成代码,并不是通过json再次渲染,结果在网页端生成代码,最后只能生成一个文件,发现了局限性很大。之前也写过通过eletron和nodejs就能通过模板生成多个文件,然后就想到了代码段,其实自己写过的代码也是一个知识库, 很多代码其实都可以复用,例如通用表单,图片上传等。但在vscode里是看不到自己配置了那些代码段,所以我明明装了很多的代码段扩展,却因为不知道里面的代码段导致每次都是看官方文档再复制,于是产生了写一个插件能够看到vscode里面所配置的代码段并且可以点击使用。
开始踩坑
项目搭建
从命令行安装Yeoman和VSCode扩展生成器
npm install -g yo generator-code
在命令行中输入如下命令来启动生成器
yo code
配置选择

视图选择
因为要展示的是类似目录的接口,可以使用树视图,也可以使用webview来实现。最后选择树视图,因为想着就放在侧边栏使用就好,比较方便。
Tree View文档:https://code.visualstudio.com/api/extension-guides/tree-view
视图开发
package.json配置
"contributes": {
"viewsContainers": {
"activitybar": [
{
"id": "snippet-viwer",
"title": "代码段查看器",
"icon": "img/left_icon.svg"
}
]
},
"views": {
"snippet-viwer": [
{
"id": "plugin-view",
"name": "扩展目录"
}
]
}
},
第二步是向你注册的视图提供数据,以便 VSCode 可以在视图中显示数据。
树节点类
export class TreeItemNode extends TreeItem {
constructor(
public readonly label: string,
public readonly icon: string,
public readonly body: string,
public readonly children: TreeChild[] | string,
public collapsibleState: TreeItemCollapsibleState
) {
super(label, collapsibleState);
this.iconPath = Uri.file(join(__filename, "..", "..", icon));
if (this.collapsibleState === TreeItemCollapsibleState.None) {
this.command = {
title: this.label,
command: "itemClick",
tooltip: this.body,
arguments: [
this.body,
],
};
}
}
tooltip = this.body;
}
TreeDataProvider实现类
export class TreeViewProvider implements TreeDataProvider<TreeItemNode> {
private _onDidChangeTreeData: EventEmitter<
TreeItemNode | undefined | null | void
> = new EventEmitter<TreeItemNode | undefined | null | void>();
private treeList: Tree[] = [];
private context: ExtensionContext;
private customDisposableList: Disposable[] = [];
constructor(context: ExtensionContext) {
this.context = context;
this.initList();
}
initList() {
this.treeList = [];
}
onDidChangeTreeData?:
| import("vscode").Event<TreeItemNode | undefined | null | void> =
this._onDidChangeTreeData.event;
getTreeItem(element: TreeItemNode): TreeItem | Thenable<TreeItem> {
console.log("获取节点", element.label);
return element;
}
getChildren(
element?: TreeItemNode | undefined
): ProviderResult<TreeItemNode[]> {
}
public static initTreeViewItem(context: ExtensionContext) {
const treeViewProvider = new TreeViewProvider(context);
window.registerTreeDataProvider("plugin-view", treeViewProvider);
return treeViewProvider;
}
}
数据获取
获取扩展的代码段数据
extensions文档:https://code.visualstudio.com/api/references/vscode-api#extensions
通过extensions.all便可以得到所有的扩展数据
通过其数据结构分析,extensionPath为扩展的目录,packageJSON->contributes->snippets为代码段的数据,便可以获取所有的代码段文件并将其载入就行
let extensionsList = extensions.all;
extensionsList = extensionsList.filter(
(item) => !!item?.packageJSON?.contributes?.snippets
);
extensionsList.forEach((item) => {
this.treeList.push({
name: item?.packageJSON?.name,
icon: "img/folder_type_plugin.svg",
children: item.packageJSON.contributes.snippets.map(
(snippetItem: SnippetManifest) => ({
name: snippetItem.language,
icon: "img/folder_type_src.svg",
children: path.join(item?.extensionPath || "", snippetItem.path),
})
),
});
});
获取自定义的代码段数据
反复查文档也找不到能够获取设置的用户自定义代码段的获取方法,但由于用户自定义的代码段都会被存放在一个文件夹下,可以通过配置路径的方法来实现。后来出现了把自定义的代码段作为一个项目的想法,这样就可以实时的同步代码段等,感觉这样子管理也很好。就只提供配置自定义代码段文件夹的方式来实现自定义代码段的导入。
this.treeList = [];
for (const disposable of this.customDisposableList) {
disposable.dispose();
}
this.customDisposableList = [];
const customConfig = workspace.getConfiguration("SnippetViewer");
if (customConfig.customUrl) {
try {
delete require.cache[join(customConfig.customUrl, "config.js")];
const customConfigList: Tree[] = JSON5.parse(
fs.readFileSync(
path.join(customConfig.customUrl, "config.json"),
"utf8"
)
);
} catch (error) {
console.error("自定义配置错误");
window.showErrorMessage("自定义代码段配置错误");
}
}
代码段的目录结构
snippets
├─ custom
│ └─ vue.json
│ └─ javacript.json
└─ config.json
config.json 数据结构
[
{
"name": "custom",
"children": [
{
"name": "vue",
"children": "custom/vue.json"
}
]
}
]
一级name表示名称,二级name表示语言
数据显示
更新TreeViewProvider
的 getChildren
逻辑
getChildren(
element?: TreeItemNode | undefined
): ProviderResult<TreeItemNode[]> {
if (element) {
if (Array.isArray(element.children)) {
return element.children.map((item) => {
return new TreeItemNode(
item.name,
item.icon,
item.name,
item.children,
TreeItemCollapsibleState.Collapsed as TreeItemCollapsibleState
);
});
} else {
let resultArr: string[] = [];
let json: { [key: string]: SnippetJSON } = {};
try {
json = JSON5.parse(
fs.readFileSync(path.join(element.children), "utf8")
);
resultArr = Object.keys(json);
} catch (error) {
console.log(error);
window.showErrorMessage("代码段文件错误");
}
return resultArr.map(
(key) =>
new TreeItemNode(
key,
"img/code.svg",
Array.isArray(json[key].body)
? (json[key].body as string[]).join("\n")
: (json[key].body as string),
"",
TreeItemCollapsibleState.None as TreeItemCollapsibleState
)
);
}
} else {
return this.treeList.map((item) => {
return new TreeItemNode(
item.name,
item.icon,
item.name,
item.children,
TreeItemCollapsibleState.Collapsed as TreeItemCollapsibleState
);
});
}
}
踩坑:
格式化json数据的时候发现有些代码段的json文件格式有错误,一开始发现的是最后有逗号,我用Prettier格式化一下就好了,所以一开始使用Prettier来进行格式化,发现还是报错,我又打开一个代码段json查看,里面竟然有注释,那时我想,json还能这样写的吗,但是vscode读取的时候也是可以读取的,那他肯定是可以读取了,然后查了一会才发现有json5这个东西,是json的一个超集,可以使用逗号结尾和注释等,json5传送门,最后通过json5来格式化json,就没问题了。
自定义代码段增加代码补全
扩展的代码段vscode是会自动加载的,我们自己通过配置的代码段目前只能通过侧边栏来点击使用,希望能够像扩展一样同时使用代码补全功能,主要通过languages.registerCompletionItemProvider
来实现。
addCustomSnippets(
language: string,
json: { [key: string]: SnippetJSON }
): void {
const disposable = languages.registerCompletionItemProvider(
{ scheme: "file", language },
{
provideCompletionItems() {
return Object.keys(json).map((key) => {
const snippetCompletion = new CompletionItem(
{
label: json[key].prefix,
description: key,
detail: "(custom)",
},
CompletionItemKind.Snippet
);
snippetCompletion.insertText = new SnippetString(
Array.isArray(json[key].body)
? (json[key].body as string[]).join("\n")
: (json[key].body as string)
);
snippetCompletion.detail = key;
snippetCompletion.documentation =
new MarkdownString().appendCodeblock(
snippetCompletion.insertText.value
);
return snippetCompletion;
});
},
}
);
this.customDisposableList.push(disposable);
this.context.subscriptions.push(disposable);
}
最后通过customDisposableList
来存储所有的注册,刷新列表时注销之前的注册再重新注册。
增加刷新命令
更新扩展或者自定义代码段时,能够通过刷新按钮刷新可视化列表。
package.json
"contributes": {
"commands": [
{
"command": "snippets-viewer.refresh",
"title": "Refresh List",
"icon": "$(refresh)"
}
],
"menus": {
"view/title": [
{
"command": "snippets-viewer.refresh",
"when": "view == plugin-view",
"group": "navigation"
}
]
}
}
TreeViewProvider
主要通过_onDidChangeTreeData
来实现刷新
refresh(): void {
this.initList();
this._onDidChangeTreeData.fire();
}
踩坑
一开始自定义的config.json是使用config.js的,通过require的方式引入,require引入之后会有缓存,需要每次重新加载都要清除之前的缓存,嫌麻烦就直接全改成json了。
最后注册代码
export function activate(context: vscode.ExtensionContext) {
console.log(
'Congratulations, your extension "snippets viewer" is now active!'
);
const treeViewProvider = TreeViewProvider.initTreeViewItem(context);
let itemClickDisposable = vscode.commands.registerCommand(
"itemClick",
(body) => {
const editor = vscode.window.activeTextEditor;
if (editor) {
editor.insertSnippet(new vscode.SnippetString(body));
}
}
);
let refreshDisposable = vscode.commands.registerCommand(
"snippets-viewer.refresh",
() => treeViewProvider.refresh()
);
context.subscriptions.push(itemClickDisposable, refreshDisposable);
}
export function deactivate() {}
总结
代码和插件已发布,搜索vscode-snippets-viewer就能搜到,喜欢的话给个star吧
github:https://github.com/shilim-developer/snippets-viewer
vscode market:https://marketplace.visualstudio.com/items?itemName=shilim.vscode-snippets-viewer