vscode插件实现扫描页面Image标签

功能

想对从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 中的位置。

核心代码

  1. 获取页面中真正需要标记的元素。
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;
}
  1. 给需要标记的元素进行装饰。
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);
}
  1. 在页面触发的时机。前两个时机基本可以满足需求了。
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);
        }
      }
    }
 )
}

  
相关推荐
疯狂的沙粒5 分钟前
Vue 前端大屏做多端屏幕适配时,如何让其自动适配多种不同尺寸的屏幕?
前端·javascript·vue.js
范小多8 分钟前
24小时学会Python Visual code +Python Playwright通过谷歌浏览器取控件元素(连载、十一)
服务器·前端·python
ooolmf9 分钟前
matlab2024读取温度01
java·前端·javascript
打工人小夏10 分钟前
前端vue3项目使用nprogress动画组件,实现页面加载动画
前端
一颗宁檬不酸12 分钟前
前端农业商城中产品产地溯源功能的实现
前端
李少兄19 分钟前
深入理解前端中的透视(Perspective)
前端·css
席之郎小果冻21 分钟前
【03】【创建型】【聊一聊,单例模式】
开发语言·javascript·单例模式
江公望29 分钟前
HTML5 History 模式 5分钟讲清楚
前端·html·html5
云和数据.ChenGuang35 分钟前
Zabbix Web 界面安装时**无法自动创建配置文件 `zabbix.conf.php`** 的问题
前端·zabbix·运维技术·数据库运维工程师·运维教程
码界奇点38 分钟前
Java Web学习 第15篇jQuery万字长文详解从入门到实战解锁前端交互新境界
java·前端·学习·jquery