功能
想对从react-native中引入的Image和ImageBackground组件进行底部波浪线标记,所在行的后面添加文字提示,并且鼠标hover出现提示弹窗。


思考
要实现这个功能,首先要知道如何判断组件是从react-native中引入的,以及react-native是否使用引入了Image和ImageBackground组件,这是一个双向判断的过程。然后就得确定需要标记的组件名称,比如这种Image as NewImage 别名的形式,就不是原名Image了,而是NewImage。这样可以保证标记的组件不会出错。然后确定好了最终需要标记的组件名称,就得在页面中找到具体的位置。
我一开始使用正则去匹配<Image../>和<ImageBackground../>的闭合标签。虽然可以正确找到在页面中的位置,但是如果出现了别名的形式(Image as NewImage ),以及这个Image或者ImageBackground不从react-native中引入的,这样就会导致标记不准确了。
js
const warnDecorationType = vscode.window.createTextEditorDecorationType({
textDecoration: "rgb(182, 138, 47) wavy underline 0.1px",
}); // 定义波浪线装饰样式
const text = editor.document.getText(); // 当前活动页的文本内容
const imageTagMatches: vscode.DecorationOptions[] = []; // 保存需要装饰的标签
const imageTagRegex =
/<(Image|ImageBackground)(\s+[^>]*?)?(?:\/>|>([\s\S]*?)<\/\1>)/g;
let match: RegExpExecArray | null;
while ((match = imageTagRegex.exec(text))) {
const startPosition = editor.document.positionAt(match.index);
const endPosition = editor.document.positionAt(
match.index + match[0].length
);
const decoration = { range: new vscode.Range(startPosition, endPosition) };
imageTagMatches.push(decoration);
}
editor.setDecorations(warnLineDecorationType, imageTagMatches); // 设置装饰
最后考虑得用ast语法树解析了,因为这个解析语法树可以查找所有import的组件,以及获取页面所有dom元素,可以拿到元素全部信息,包括名称,位置等。这正是我们所需要的。所以我们使用两个工具@babel/parser和@babel/traverse。大家也可以使用AST explorer去看生成的语法树结构。
- @babel/parser 是一个解析器,可以将 JavaScript 代码转换为 AST(抽象语法树)。它支持解析最新版的 ECMAScript 标准,并具有一个可扩展的插件系统,允许添加自定义的解析器。
- @babel/traverse 则是一个遍历器,可以深入遍历 AST,并访问、修改节点。它支持基于事件的遍历模式,从而能够优化性能。它还具有一个路径(path)对象,可以跟踪到当前遍历节点在 AST 中的位置。
核心代码
- 获取页面中真正需要标记的元素。
jsx
// getWarnImage.ts
import * as vscode from "vscode";
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
export default function (document: vscode.TextDocument) {
const warnImageTags: any = []; // 保存需要标记的标签
function parseTsxCode(code) {
return parse(code, {
sourceType: "module",
plugins: ["jsx", "typescript", "classProperties", "decorators-legacy"],
});
}
const text = document.getText();
const ast = parseTsxCode(text);
const elements = []; // 保存页面所有dom元素
traverse(ast, {
JSXOpeningElement(path) { // 获取开标签元素,比如<div>...</div>,只获取前半个div的信息
elements.push(path.node);
},
});
const componentNames = []; // 保存需要标记的组件名称
// 遍历AST并查找所有import的组件
traverse(ast, {
ImportDeclaration(path) {
const specifiers = path.node.specifiers;
const source = path.node.source.value;
if (source === "react-native") { // 判断是从react-native中引入的组件
if (!specifiers[0].imported) {
// 考虑import Native from 'react-native';这种写法
componentNames.push(
...[
`${specifiers[0].local.name}.Image`,
`${specifiers[0].local.name}.ImageBackground`,
]
);
return;
}
for (let i = 0, l = specifiers.length; i < l; i++) {
const specifier = specifiers[i];
const oldName = specifier.imported.name;
const newName = specifier.local.name; // 别名后得用local里面获取最终的名称
if (oldName === "Image" || oldName === "ImageBackground") {
componentNames.push(newName);
}
}
}
},
});
elements.forEach((item) => {
// 如果是import Native from 'react-native';这种写法,页面中使用就是 <Native.Image ../>,这个时候获取名称就是另外一种写法。
let newName = item.name.name
? item.name.name
: item.name.object.name + "." + item.name.property.name;
if (componentNames.includes(newName)) {
warnImageTags.push(item);
}
});
return warnImageTags;
}
- 给需要标记的元素进行装饰。
jsx
// scanImage.ts
import * as vscode from "vscode";
import getWarnImage from "./getWarnImage";
let warnLineDecorationType: vscode.TextEditorDecorationType;
let warnTextDecorationType: vscode.TextEditorDecorationType;
export default function (editor: vscode.TextEditor) {
if (!editor) { // 这里需要判断下,不然编辑器里面没有打开的文件的时候,插件会报错
return;
}
if (warnTextDecorationType) { // 清除以前的标记对象,保证每一次页面变化都会重新标记
warnTextDecorationType.dispose();
}
if (warnLineDecorationType) {
warnLineDecorationType.dispose();
}
// 定义装饰文案
warnTextDecorationType = vscode.window.createTextEditorDecorationType({
after: {
contentText: ` ⚠️⚠️推荐使用@kds/image。详情见 https://ksurl.cn/SgHR5tpv`,
color: "rgb(182, 138, 47)",
},
});
// 定义装饰破浪线样式
warnLineDecorationType = vscode.window.createTextEditorDecorationType({
textDecoration: "rgb(182, 138, 47) wavy underline",
});
const componentNames = getWarnImage(editor.document); // 需要装饰的元素列表
const imageTagMatches: vscode.DecorationOptions[] = [];
const imageNamesMatches: any = [];
componentNames.length &&
componentNames.forEach((item: any) => {
// 在组件名称所在的这一行后面添加文案提示
const curLine = editor.document.lineAt(item.name.loc.start.line - 1);// 组件名称所在行
const startPosition = editor.document.positionAt(item.name.start);//装饰起始位置
const endPosition = editor.document.positionAt( // 装饰结束位置
item.name.start + curLine.text.trim().length - 1
);
const decoration = {
range: new vscode.Range(startPosition, endPosition),
hoverMessage: // 定义hover内容
"推荐使用@kds/image。详情见 [https://ksurl.cn/SgHR5tpv](https://ksurl.cn/SgHR5tpv)",
};
imageTagMatches.push(decoration);
// 对组件名称添加破浪线装饰
const nameStartPosition = editor.document.positionAt(item.name.start);
const nameEndPosition = editor.document.positionAt(item.name.end);
const nameDecoration = {
range: new vscode.Range(nameStartPosition, nameEndPosition),
};
imageNamesMatches.push(nameDecoration);
});
editor.setDecorations(warnTextDecorationType, imageTagMatches);
editor.setDecorations(warnLineDecorationType, imageNamesMatches);
}
- 在页面触发的时机。前两个时机基本可以满足需求了。
jsx
// extensin.ts
import decorateImageTags from './utilities/scanImage';
export async function activate(context: ExtensionContext): Promise<void> {
decorateImageTags(vscode.window.activeTextEditor); // 1. 首次进入页面触发
// 2. 文档(文件)的内容发生更改时触发
vscode.workspace.onDidChangeTextDocument(
async (e) => {
if (e.contentChanges.length > 0) {
decorateImageTags(vscode.window.activeTextEditor);
}
},
null,
context.subscriptions
);
// 3. VS Code窗口中的活动文本编辑器更改时触
vscode.window.onDidChangeActiveTextEditor(
async (e: vscode.TextEditor | undefined) => {
if (e && supportedFilesSelector.includes(e.document.languageId)) {
try {
decorateImageTags(e);
} catch (e) {
console.log(e);
}
}
}
)
}