最近在研究低代码,发现很多的低代码都是表单生成器。自己也照猫画虎地造了一下轮子,因为一开始的方向就是生成代码,并不是通过json再次渲染,结果在网页端生成代码,最后只能生成一个文件,发现了局限性很大。之前也写过通过eletron和nodejs就能通过模板生成多个文件,然后就想到了代码段,其实自己写过的代码也是一个知识库, 很多代码其实都可以复用,例如通用表单,图片上传等。但在vscode里是看不到自己配置了那些代码段,所以我明明装了很多的代码段扩展,却因为不知道里面的代码段导致每次都是看官方文档再复制,于是产生了写一个插件能够看到vscode里面所配置的代码段并且可以点击使用。
从命令行安装Yeoman和VSCode扩展生成器
npm install -g yo generator-code
在命令行中输入如下命令来启动生成器
yo code
配置选择
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WgjO3G6Q-1661322420979)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9726f61977ad4f10ac9933551427cd37~tplv-k3u1fbpfcp-zoom-1.image)]
因为要展示的是类似目录的视图,可以使用树视图,也可以使用webview来实现。最后选择树视图,因为想着就放在侧边栏使用就好,比较方便。
Tree View文档:https://code.visualstudio.com/api/extension-guides/tree-view
"contributes": {
// 图标栏
"viewsContainers": {
"activitybar": [
{
"id": "snippet-viwer", // id对应视图id
"title": "代码段查看器",
"icon": "img/left_icon.svg"
}
]
},
// 视图栏
"views": {
"snippet-viwer": [
{
"id": "plugin-view",
"name": "扩展目录"
}
]
}
},
第二步是向你注册的视图提供数据,以便 VSCode 可以在视图中显示数据。
主要设置节点的图标,label名称等
export class TreeItemNode extends TreeItem {
constructor(
// readonly 只可读
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));
// command: 为每项添加点击事件的命令
if (this.collapsibleState === TreeItemCollapsibleState.None) {
this.command = {
title: this.label, // 标题
command: "itemClick", // 命令 ID
tooltip: this.body, // 鼠标覆盖时的小小提示框
arguments: [
// 向 registerCommand 传递的参数。
this.body,
],
};
}
}
tooltip = this.body;
}
主要通过实现getChildren方法来返回每一级的列表数据
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) {
// 实例化 TreeViewProvider
const treeViewProvider = new TreeViewProvider(context);
// registerTreeDataProvider:注册树视图
window.registerTreeDataProvider("plugin-view", treeViewProvider);
return treeViewProvider;
}
}
extensions文档:https://code.visualstudio.com/api/references/vscode-api#extensions
通过extensions.all便可以得到所有的扩展数据,数据结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UK6ndA1v-1661322420981)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f93e70ca8009442cacf56c3c420d0a55~tplv-k3u1fbpfcp-zoom-1.image)]
通过其数据结构分析,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表示语言
因为在开发过程中也在不断查看学习其他插件的代码,然后在Settings Sync发现新大陆,在他的environmentPath.ts文件里面有个USER_FOLDER
的常量,他就是用户的数据存放的目录路径,而用户自定义代码段就是存放在其下面的snippets文件夹下,这样,用户自定义代码段数据也能解决了。
constructor(context: vscode.ExtensionContext) {
this.isPortable = !!process.env.VSCODE_PORTABLE;
if (!this.isPortable) {
this.path = resolve(context.globalStorageUri.fsPath, "../../..").concat(
normalize("/")
);
this.userFolder = resolve(this.path, "User").concat(normalize("/"));
} else {
this.userFolder = resolve(this.path, "user-data/User").concat(
normalize("/")
);
}
this.snippetsFolder = this.userFolder.concat("/snippets/");
}
// 加载用户自定义代码段
try {
this.treeList.push({
name: "user-snippets",
icon: "vscode",
body: "user-snippets",
isOutCustomRoot: false,
expression: `${this.treeList.length}`,
disabled: false,
children: fs
.readdirSync(this.environment.snippetsFolder)
.map((fileName, fileIndex) => {
return {
name: fileName.substring(0, fileName.lastIndexOf(".")),
icon: "src",
isOutCustomRoot: false,
disabled: false,
expression: `${this.treeList.length}.${fileIndex}`,
children: join(this.environment.snippetsFolder, fileName),
};
}),
});
} catch (error) {}
更新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 {
// 不包含elment, 根节点
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
来实现。
/**
* 增加自定义代码段
* @param {string} language
* @param {object} json
*/
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
来存储所有的注册,刷新列表时注销之前的注册再重新注册。
更新扩展或者修改自定义代码段时,能够通过刷新按钮刷新可视化列表。
"contributes": {
"commands": [
{
"command": "snippets-viewer.refresh",
"title": "Refresh List",
"icon": "$(refresh)"
}
],
"menus": {
"view/title": [
{
"command": "snippets-viewer.refresh",
"when": "view == plugin-view",
"group": "navigation"
}
]
}
}
主要通过_onDidChangeTreeData
来实现刷新
/**
* 刷新列表
*/
refresh(): void {
this.initList();
this._onDidChangeTreeData.fire();
}
踩坑
一开始自定义的config.json是使用config.js的,通过require的方式引入,require引入之后会有缓存,需要每次重新加载都要清除之前的缓存,嫌麻烦就直接全改成json了。
最后注册代码
export function activate(context: vscode.ExtensionContext) {
// Use the console to output diagnostic information (console.log) and errors (console.error)
// This line of code will only be executed once when your extension is activated
console.log(
'Congratulations, your extension "snippets viewer" is now active!'
);
// 实现树视图的初始化
const treeViewProvider = TreeViewProvider.initTreeViewItem(context);
// The command has been defined in the package.json file
// Now provide the implementation of the command with registerCommand
// The commandId parameter must match the command field in package.json
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);
}
// this method is called when your extension is deactivated
export function deactivate() {}
一开始实现图标是在vscode里面扣了几个出来,但是发现他是一成不变的,然后也想着能够通过修改element的状态来实现,但是他并不会调用getTreeItem
和getChildren
。也看了leetcode的代码,主要里面有文件夹展开的,但是还是搞不懂他是怎么做到的。最后终于被我搞明白了T_T,在TreeItem
里面有个resourceUri
属性
/**
* The {@link Uri} of the resource representing this item.
*
* Will be used to derive the {@link TreeItem.label label}, when it is not provided.
* Will be used to derive the icon from current file icon theme, when {@link TreeItem.iconPath iconPath} has {@link ThemeIcon} value.
*/
resourceUri?: Uri;
就是在不设置icon的情况下,可以通过设置resourceUri
来展示系统的图标,如果有主题就会使用主题。resourceUri
的类型是Uri
,其中能够影响的是path
,例如path = ‘/src’,他就会显示src的图标,并且在节点被展开折叠时会自动显示展开折叠样式。
上面的代码仅仅是开发的过程思路,最终的代码也发生了改变。后面也根据issue实现了一下禁用外部自定义代码段和根据语言来分组的功能,感兴趣的话可以自行阅读源码。
代码和插件已发布,搜索vscode-snippets-viewer就能搜到,喜欢的话给个star吧
github:https://github.com/shilim-developer/snippets-viewer
vscode market:https://marketplace.visualstudio.com/items?itemName=shilim.vscode-snippets-viewer