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 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子5 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年5 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子5 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina5 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路6 小时前
React--Fiber 架构
前端·react.js·架构
coderlin_6 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
伍哥的传说6 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
qq_424409196 小时前
uniapp的app项目,某个页面长时间无操作,返回首页
前端·vue.js·uni-app
我在北京coding6 小时前
element el-table渲染二维对象数组
前端·javascript·vue.js