整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

如何编写属于自己的 PostCSS 8 插件?

者近期在将前端架构 webpack 升级到 5 时,一些配套模块也需要进行升级,其中包括了 css 处理模块 PostCSS。旧版本使用的是 PostCSS 7,在升级至 PostCSS 8 的过程中,笔者发现部分插件前置依赖还是停留在 7 版本,且年久失修,在 PostCSS 8 中出现各种各样的问题,无奈只能研究源码,将目前部分旧版本插件升级至新版本。这里,笔者将升级插件的过程进行简化和提炼,让读者自己也可以编写一个 PostCSS 8 插件。

插件工作原理

PostCSS 是一个允许使用 JS 插件转换样式的工具。开发者可以根据自己的实际需求,在编译过程将指定 css 样式进行转换和处理。目前 PostCSS 官方收录插件有 200 多款,其中包括使用最广泛的Autoprefixer自动补全 css 前缀插件。

PostCSS 和插件的工作原理其实很简单,就是先将 css 源码转换为 AST,插件基于转换后 AST 的信息进行个性化处理,最后 PostCSS 再将处理后的 AST 信息转换为 css 源码,完成 css 样式转换,其流程可以归结为下图:

下面我们通过实际例子看看 PostCSS 会将 css 源码转换成的 AST 格式:

const postcss = require('postcss')
postcss().process(`
.demo {
 font-size: 14px; /*this is a comment*/
}
`).then(result => {
 console.log(result)
})

复制代码

代码中直接引用 PostCSS,在不经过任何插件的情况下将 css 源码进行转换,AST 转换结果如下:

{
 "processor": {
 "version": "8.3.6",
 "plugins": []
 },
 "messages": [],
 "root": {
 "raws": {
 "semicolon": false,
 "after": "\n"
 },
 "type": "root",
 // ↓ nodes字段内容重点关注
 "nodes": [
 {
 "raws": {
 "before": "\n",
 "between": " ",
 "semicolon": true,
 "after": "\n"
 },
 "type": "rule",
 "nodes": [
 {
 "raws": {
 "before": "\n ",
 "between": ": "
 },
 "type": "decl",
 "source": {
 "inputId": 0,
 "start": {
 "offset": 11,
 "line": 3,
 "column": 3
 },
 "end": {
 "offset": 26,
 "line": 3,
 "column": 18
 }
 },
 "prop": "font-size", // css属性和值
 "value": "14px"
 },
 {
 "raws": {
 "before": " ",
 "left": "",
 "right": ""
 },
 "type": "comment", // 注释类
 "source": {
 "inputId": 0,
 "start": {
 "offset": 28,
 "line": 3,
 "column": 20
 },
 "end": {
 "offset": 48,
 "line": 3,
 "column": 40
 }
 },
 "text": "this is a comment"
 }
 ],
 "source": {
 "inputId": 0,
 "start": {
 "offset": 1,
 "line": 2,
 "column": 1
 },
 "end": {
 "offset": 28,
 "line": 4,
 "column": 1
 }
 },
 "selector": ".demo", // 类名
 "lastEach": 1,
 "indexes": {}
 }
 ],
 "source": {
 "inputId": 0,
 "start": {
 "offset": 0,
 "line": 1,
 "column": 1
 }
 },
 "lastEach": 1,
 "indexes": {},
 "inputs": [
 {
 "hasBOM": false,
 "css": "\n.demo {\n font-size: 14px;\n}\n",
 "id": "<input css vi1Oew>"
 }
 ]
 },
 "opts": {},
 "css": "\n.demo {\n font-size: 14px;\n}\n"
}

复制代码

AST 对象中 nodes 字段里的内容尤为重要,其中存储了 css 源码的关键字、注释、源码的起始、结束位置以及 css 的属性和属性值,类名使用selector存储,每个类下又存储一个 nodes 数组,该数组下存放的就是该类的属性(prop)和属性值(value)。那么插件就可以基于 AST 字段对 css 属性进行修改,从而实现 css 的转换。

PostCSS 插件格式规范及 API

PostCSS 插件其实就是一个 JS 对象,其基本形式和解析如下:

module.exports = (opts = { }) => {
 // 此处可对插件配置opts进行处理
 return {
 postcssPlugin: 'postcss-test', // 插件名字,以postcss-开头
 
Once (root, postcss) {
 // 此处root即为转换后的AST,此方法转换一次css将调用一次
 },
 
Declaration (decl, postcss) {
 // postcss遍历css样式时调用,在这里可以快速获得type为decl的节点(请参考第二节的AST对象)
 },
 
Declaration: {
 color(decl, postcss) {
 // 可以进一步获得decl节点指定的属性值,这里是获得属性为color的值
 }
 },
 
Comment (comment, postcss) {
 // 可以快速访问AST注释节点(type为comment)
 },
 
AtRule(atRule, postcss) {
 // 可以快速访问css如@media,@import等@定义的节点(type为atRule)
 }
 
}
}
module.exports.postcss = true

复制代码

更多的 PostCSS 插件 API 可以详细参考官方postcss8文档,基本原理就是 PostCSS 会遍历每一个 css 样式属性值、注释等节点,之后开发者就可以针对个性需求对节点进行处理即可。

实际开发一个 PostCSS 8 插件

了解了 PostCSS 插件的格式和 API,我们将根据实际需求来开发一个简易的插件,有如下 css:

.demo {
 font-size: 14px; /*this is a comment*/
 color: #ffffff;
}

复制代码

需求如下:

  1. 删除 css 内注释
  2. 将所有颜色为十六进制的#ffffff转为 css 内置的颜色变量white

根据第三节的插件格式,本次开发只需使用Comment和Declaration接口即可:

// plugin.js
module.exports = (opts = { }) => {
 return {
 postcssPlugin: 'postcss-test',
 
Declaration (decl, postcss) {
 if (decl.value === '#ffffff') {
 decl.value = 'white'
 }
 },
 
Comment(comment) {
 comment.text = ''
 }
 
}
}
module.exports.postcss = true

复制代码

在 PostCSS 中使用该插件:

// index.js
const plugin = require('./plugin.js')
postcss([plugin]).process(`
.demo {
 font-size: 14px; /*this is a comment*/
 color: #ffffff;
}
`).then(result => {
 console.log(result.css)
})

复制代码

运行结果如下:

.demo {
 font-size: 14px; /**/
 color: white;
}

复制代码

可以看到,字体颜色值已经成功做了转换,注释内容已经删掉,但注释标识符还依旧存在,这是因为注释节点是包含/**/内容存在的,只要 AST 里注释节点还存在,最后 PostCSS 还原 AST 时还是会把这段内容还原,要做到彻底删掉注释,需要对 AST 的 nodes 字段进行遍历,将 type 为 comment 的节点进行删除,插件源码修改如下:

// plugin.js
module.exports = (opts = { }) => {
 // Work with options here
 // https://postcss.org/api/#plugin
 return {
 postcssPlugin: 'postcss-test',
 
Once (root, postcss) {
 // Transform CSS AST here
 root.nodes.forEach(node => {
 if (node.type === 'rule') {
 node.nodes.forEach((n, i) => {
 if (n.type === 'comment') {
 node.nodes.splice(i, 1)
 }
 })
 }
 })
 },
 

Declaration (decl, postcss) {
 // The faster way to find Declaration node
 if (decl.value === '#ffffff') {
 decl.value = 'white'
 }
 }
 
}
}
module.exports.postcss = true

复制代码

重新执行 PostCSS,结果如下,符合预期。

.demo {
 font-size: 14px;
 color: white;
}

复制代码

插件开发注意事项

通过实操开发可以看到,开发一个 PostCSS 插件其实很简单,但在实际的插件开发中,开发者需要注意以下事项:

1.尽量使插件简单,使用者可以到手即用

Build code that is short, simple, clear, and modular.

尽量使你的插件和使用者代码解耦,开放有限的 API,同时开发者在使用你的插件时从名字就可以知道插件的功能。这里推荐一个简单而优雅的 PostCSS 插件postcss-focus,读者可以从这个插件的源码中体会这个设计理念。

2.开发插件前确认是否有现成的轮子

如果你对自己的项目有个新点子,想自己开发一个插件去实现,在开始写代码前,可以先到 PostCSS 官方注册的插件列表中查看是否有符合自己需求的插件,避免重复造轮子。不过截止目前(2021.8),大部分插件依旧停留在 PostCSS 8 以下,虽然 PostCSS 8 已经对旧版本插件做了处理,但在 AST 的解析处理上还是有差异,从实际使用过程中我就发现 PostCss8 使用低版本插件会导致 AST 内的source map丢失,因此目前而言完全兼容 PostCSS 8 的插件还需各位开发者去升级。

从低版本 PostCSS 迁移

升级你的 PostCSS 插件具体可以参考官方给出的升级指引。这里只对部分关键部分做下解释:

1.升级 API

  • 将旧版module.exports = postcss.plugin(name, creator)替换为module.exports = creator;
  • 新版插件将直接返回一个对象,对象内包含Once方法回调;
  • 将原插件逻辑代码转移至Once方法内;
  • 插件源码最后加上module.exports.postcss = true;

具体示例如下。

旧版插件:

- module.exports = postcss.plugin('postcss-dark-theme-class', (opts = {}) => {
- checkOpts(opts)
- return (root, result) => {
 root.walkAtRules(atrule => { … })
- }
- })

复制代码

升级后插件:

+ module.exports = (opts = {}) => {
+ checkOpts(opts)
+ return {
+ postcssPlugin: 'postcss-dark-theme-class',
+ Once (root, { result }) {
 root.walkAtRules(atrule => { … })
+ }
+ }
+ }
+ module.exports.postcss = true

复制代码

2.提取逻辑代码至新版 API

把逻辑代码都放在Once回调内还不够优雅,PostCSS 8 已经实现了单个 css 的代码扫描,提供了Declaration(), Rule(), AtRule(), Comment() 等方法,旧版插件类似root.walkAtRules的方法就可以分别进行重构,插件效率也会得到提升:

module.exports = {
 postcssPlugin: 'postcss-dark-theme-class',
- Once (root) {
- root.walkAtRules(atRule => {
- // Slow
- })
- }
+ AtRule (atRule) {
+ // Faster
+ }
 }
 module.exports.postcss = true

复制代码

总结

通过本文的介绍,读者可以了解 PostCSS 8 工作的基本原理,根据具体需求快速开发一个 PostCSS 8 插件,并在最后引用官方示例中介绍了如何快速升级旧版 PostCSS 插件。目前 PostCSS 8 还有大量还没进行升级兼容的 PostCSS 插件,希望读者可以在阅读本文后可以获得启发,对 PostCSS 8 的插件生态做出贡献。

信大家平时在电脑上逛掘金、知乎网站时,肯定有看到过下面超级烦人的跳转拦截确认页面

虽然这种拦截的初衷是好的,但是我相信大家平时肯定不会因为有了这个拦截提醒页面,就会对即将打开的网站安全性提高自己的警惕性,而是把它当做用户协议一样无视并点击“继续访问”。这种体验给人的感觉是十分难受的,特别是有时候看一些技术文章,文章里面会贴一些参考资料链接,有时我会习惯先右键新tab中打开,并且继续往下阅读,等看到刚打开的tab栏没有加载圈圈时(说明页面已经加载完毕),再切过去看,结果被拦截了???

上面的痛点,其实很容易解决,就是通过开发一个浏览器插件实现。

实现思路

我们先打开控制台看下这些网站跳转链接长啥样:

掘金:

知乎:

可以看到,a标签的链接里面并不是直接放置我们要跳转网站链接,而是把它放在了target参数里面。我们要做的就是通过插件,给页面添加点击监听事件,先拦截a标签的默认跳转行为,然后通过js提取到我们要跳转的链接,通过window.open或者window.localtion打开即可。

开始动手开发插件

新建manifest.json配置文件

首先我们新建个项目文件夹,命名direct-link,在里面新建manifest.json配置文件,里面存放我们插件的配置信息。内容如下:

{
  "name": "direct link", // 插件名字
  "description": "跳过网站点击跳转询问页面!", // 插件描述
  "version": "0.0.1", // 版本号
  "manifest_version": 3, // 插件版本,目前大多插件还是2, 3是目前最新规范标准
  "permissions": ["storage", "tabs", "scripting"], // 插件需要用到的权限
  "background": {
    "service_worker": "./background.js" // 对应background.js文件,相当于程序运行入口
  },
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "/images/logo16.png",
      "32": "/images/logo32.png",
      "48": "/images/logo48.png",
      "128": "/images/logo128.png"
    }
  },
  "icons": {
    "16": "/images/logo16.png",
    "32": "/images/logo32.png",
    "48": "/images/logo48.png",
    "128": "/images/logo128.png"
  }
}

新建images文件夹

新建images文件夹,里面存放插件的图标。我们可以去iconfont网站查找下载图片即可,尺寸需要下载多个,从上面配置文件可以看到一共放了16,32,48及128四个分辨率的图片。

新建background.js

在根目录下新建background.js,该文件相当于程序运行入口。创建background.js文件之后,此时准备的文件已经可以在浏览器中运行了。我们按如下图方式打开浏览器插件页面

然后将右上角的开发者模式打开

接着将direct link文件夹直接拖到当前页面即可看到插件成功安装

这里值得一提的是,上面manifest.json文件中在两处地方配置了logo信息,上图看到的插件图标对应的是icon属性,而action ->default_icon 对应的是下图中的图标显示位置:

编辑background.js

上面有提到,background.js相当于程序主入口,内容如下:

// 用户首次安装插件时执行一次,后面不会再重新执行。(除非用户重新安装插件)
chrome.runtime.onInstalled.addListener(() => {
  // 插件功能安装默认启用  
  chrome.storage.sync.set({
    linkOpen: true,
  });
});

// 监听tab页面加载状态,添加处理事件
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  // 设置判断条件,页面加载完成才添加事件,否则会导致事件重复添加触发多次
  if (changeInfo.status === "complete" && /^http/.test(tab.url)) {
    chrome.scripting
      .executeScript({
        target: { tabId: tabId },
        files: ["./content-script.js"],
      })
      .then(() => {
        console.log("INJECTED SCRIPT SUCC.");
      })
      .catch((err) => console.log(err));
  }
});

上面的代码逻辑比较简单,插件安装初始化时,在本地存储一个变量linkOpen设为true,后面我们会新增一个选项切换是否启用插件,需要用到这个变量判断。

接着在页面初始化时,添加执行脚本代码,这个脚本代码叫content-script,里面执行我们功能代码逻辑。

编辑content-script.js

在根目录新建content-script.js,编辑内容如下:

chrome.storage.sync.get("linkOpen", ({ linkOpen }) => {
  if (linkOpen) {
    document.body.addEventListener("click", function (event) {
      const target = event.target;
      // 判断点击的是否a标签
      if (target.nodeName.toLocaleLowerCase() === "a") {
        const href = target.getAttribute("href");
        if (href.indexOf("://link") > -1) {
          // 禁止默认的跳转行为
          event.preventDefault();
          const link = href.split("target=")[1];
          const url = decodeURIComponent(link);
          // 处理完 a 标签的内容,重新触发跳转,根据原来 a 标签页 target 来判断是否需要新窗口打开
          if (target.getAttribute("target") === "_blank") {
            // 新窗口打开  
            window.open(url);
          } else {
            // 当前窗口打开  
            window.location.href = url;
          }
        }
      }
    });
  }
});

插件主逻辑如上,对应文章开头提到的实现思路。

添加是否启用插件的功能开关

在浏览器右上角插件点击时,通常会显示一个功能菜单,如下图

下面我们也添加一个类似的功能,用来是否启用插件。

新建popup.js和popup.html

popup.html对应点击时显示的内容,popup.js则是相关执行逻辑。

popup.html:

<!DOCTYPE html>
<html lang="en">
<html>
<meta charset="UTF-8">

<head>
    <style>
        .option{padding:30px 0;display:flex;align-items:center;justify-content:center;min-width:160px}.option .name{color:#333;font-size:18px;font-weight:bold}.switch{position:relative;display:inline-block;width:60px;height:34px}.switch input{opacity:0;width:0;height:0}.slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#ccc;-webkit-transition:.4s;transition:.4s}.slider:before{position:absolute;content:"";height:26px;width:26px;left:4px;bottom:4px;background-color:white;-webkit-transition:.4s;transition:.4s}input:checked+.slider{background-color:#45c7d8}input:focus+.slider{box-shadow:0 0 1px #45c7d8}input:checked+.slider:before{-webkit-transform:translateX(26px);-ms-transform:translateX(26px);transform:translateX(26px)}.slider.round{border-radius:34px}.slider.round:before{border-radius:50%}
    </style>
</head>

<body>
    <div class="option">
        <span class="name">开启:</span>
        <label class="switch">
            <input type="checkbox" id="switch">
            <span class="slider round"></span>
        </label>
    </div>

    <script src="popup.js"></script>
</body>

</html>

显示效果如下:

popup.js

const btn = document.querySelector("#switch");

chrome.storage.sync.get("linkOpen", ({ linkOpen }) => {
  btn.checked = linkOpen;
});

btn.addEventListener("change", () => {
  if (btn.checked) {
    chrome.storage.sync.set({ linkOpen: true });
  } else {
    chrome.storage.sync.set({ linkOpen: false });
  }
  // 获取当前tab窗口
  chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
    chrome.scripting.executeScript({
      target: { tabId: tabs[0].id },
      func: refreshPage,
    });
  });
});

// 刷新页面
function refreshPage() {
  window.location.reload();
}

上面的逻辑也很简单,就是监听swich按钮,更新本地存储变量,并且每次修改时刷新下页面触发content-script里面的逻辑

至此,我们的一个插件就开发完成了。

值得注意的事

开发调试踩坑

  1. 每次修改代码后,我们需要点击如下图的刷新按钮,并且重新刷新页面,否则可能出现代码没更新触发的情况

  1. 如果出现报错信息,更新代码后重新点击刷新按钮,错误可能依然还在

点击“错误”按钮

然后点击右上角的全部清除,再重新刷新即可

  1. background.js文件代码中的console.log不会在控制台显示

我们在background.js文件中添加的打印代码是不会在浏览器的控制台打印出来的,因为它有个单独的控制台显示。入口如下图:

点击service worker会出现一个单独的调试窗口,background.js里面添加打印代码会在这个窗口的控制台中显示打印信息。

插件访问页面权限问题

如果你有按照上面内容一步步实现的话,将鼠标移动到浏览器右上角插件图标,你会发现如下图所示:

也就是说目前其实你的插件没有访问网站内容的权限,此时你需要手动点击该插件图标才能成功获得访问网站的权限。那要如何配置默认获得访问所有网站的权限呢?经过漫长的查找,发现是需要在manifest.json文件中添加这么一个属性

"host_permissions": ["https://*/*"]

添加该属性之后,右键点击图标,可以看到默认可读取更改属性是所有网站上

发布到chrome应用商店

发布插件到应用商店需要注册开发者身份,如下图所示

额,需要5美元注册费,本文结束。(感兴趣的可以自己花钱注册提交试试,哈哈)

ntelliJ IDEA 是目前最好用的 JAVA 开发 IDE,它本身的功能已经非常强大了,但是每个人的需求不一样,有些需求 IDEA 本身无法满足,于是我们就需要自己开发插件来解决。工欲善其事,必先利其器,想要提高开发效率,我们可以借助 IDEA 提供的插件功能来满足我们的需求。如果没有我需要的功能怎么办?很简单,我们自己造一个!

插件能做什么?

IDEA 的插件几乎可以做任何事情,因为它把 IDE 本身的能力都封装好开放出来了。主要的插件功能包含以下四种:

  • 自定义语言支持:如果有 IDEA 暂时不支持的语言,你可以自己写一个插件来支持,例如 Go 语言原来的支持就是通过插件做的,后来单独做了一个 Goland。官方有自定义语言插件支持的教程。
  • 框架支持:例如Struts 2 的框架支持
  • 工具集成:可以给 IDEA 的自带功能进行增强,例如对 Git 的操作增加 CodeReview 的功能。参考Gerrit
  • 用户界面:自定义的插件改变用户界面。参考BackgroundImage

我为了减少重复代码的编写,写了一个代码生成的插件IDEA代码生成插件CodeMaker,支持自定义代码生成的模板。

Hello world 插件

依照惯例,我们从 Hello world 开始。

新建一个 Gradle 的插件工程

有些教程推荐用 IDEA 默认的插件工程来开始,但是我比较推荐用 Gradle 来管理整个插件工程,后面的依赖管理会很方便,否则都得靠手动管理。

点击新建工程,选择 Gradle

接下来填写项目属性

配置 Gradle,用默认配置就行

新建完工程之后,IDEA 会自动开始解析项目依赖,因为它要下载一个几百兆的 SDK 依赖包,所以会比较久,打开科学上网能快一点。

Gradle 依赖解析完成之后,项目结构如下图,其中 plugin.xml 是插件的配置,build.gradle 是项目依赖的配置(类比 pom.xml)。

下面就是默认生成的 plugin.xml

<idea-plugin>
 <!--插件id-->
 <id>com.xiaokai.test.demo</id>
 <!--插件名称-->
 <name>Demo</name>
 <!--开发者信息-->
 <vendor email="support@yourcompany.com" url="http://www.yourcompany.com">YourCompany</vendor>
 <!--插件说明-->
 <description><![CDATA[
 Enter short description for your plugin here.<br>
 <em>most HTML tags may be used</em>
 ]]></description>
 <!-- please see http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/plugin_compatibility.html
 on how to target different products -->
 <!-- uncomment to enable plugin in all products
 <depends>com.intellij.modules.lang</depends>
 -->
 <!--依赖的其他插件能力-->
 <extensions defaultExtensionNs="com.intellij">
 <!-- Add your extensions here -->
 </extensions>
 <!--插件动作-->
 <actions>
 <!-- Add your actions here -->
 </actions>
</idea-plugin>

创建一个 Action

Action 是 IDEA 中对事件响应的处理器,它的 actionPerformed 就像是 JS 中的 onClick 方法。可以看出来,插件的开发本质上跟 web、Android 的开发没有什么不同,因为都是事件驱动的编程。

我们可以直接使用 IDEA 提供的 Action 生成器

点击 OK 之后会在 src 生成类文件:

package com.xiaokai.test;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
public class HelloWorldAction extends AnAction {
 @Override
 public void actionPerformed(AnActionEvent e) {
 // TODO: insert action logic here
 }
}

同时,动作的信息也会注册到 plugin.xml 中

 <!--插件动作-->
 <actions>
 <!-- Add your actions here -->
 <action id="demo.hello.world" class="com.xiaokai.test.HelloWorldAction" text="HelloWorld"
 description="Say Hello World">
 <add-to-group group-id="GenerateGroup" anchor="last"/>
 </action>
 </actions>

弹出对话框

创建完 Action 之后我们就要开始往里面写逻辑了,既然是 Hello World 教学,那我们就来试一下最简单的弹出对话框。

 @Override
 public void actionPerformed(AnActionEvent e) {
 //获取当前在操作的工程上下文
 Project project = e.getData(PlatformDataKeys.PROJECT);
 //获取当前操作的类文件
 PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE);
 //获取当前类文件的路径
 String classPath = psiFile.getVirtualFile().getPath();
 String title = "Hello World!";
 //显示对话框
 Messages.showMessageDialog(project, classPath, title, Messages.getInformationIcon());
 }

代码写完之后,打开 Gradle 的界面,点击 runIde 就会启动一个安装了插件的 IDEA,然后就可以进行测试。你还可以右键启动 Debug 模式,这样还能进行断点。

运行的效果如下图:

可以看到,我们右键打开 Generate 菜单之后,里面最后一项就是我们添加的 Action,

进阶的教程

如果想学习更多的原理和设计理念可以看IntelliJ Platform SDK的官方文档。不过老实说,它的文档写的挺差的,基本上就是简单讲了一下概念和原理,没有深入的分析。所以如果要深入研究还得靠自己。最靠谱的学习方式就是看别人写的插件,举个例子,你想知道怎么样实现自动生成代码,你就去找支持这个功能的插件,看他的源码是怎么写的。

我当时写CodeMaker的时候也是靠自己啃源码之后写出来的。下面我简单介绍一下我用过的一些 API,这些 API 基本都没有文档说明,全靠代码相传。

判断当前光标选择的元素是什么

 //获取当前事件触发时,光标所在的元素
 PsiElement psiElement = anActionEvent.getData(LangDataKeys.PSI_ELEMENT);
 //如果光标选择的不是类,弹出对话框提醒
 if (psiElement == null || !(psiElement instanceof PsiClass)) {
 Messages.showMessageDialog(project, "Please focus on a class", "Generate Failed", null);
 return;
 }

获取当前类文件的所有类对象

一个类文件中可能会有内部类,所以读取的时候返回的是一个列表

 public static List<PsiClass> getClasses(PsiElement element) {
 List<PsiClass> elements = Lists.newArrayList();
 List<PsiClass> classElements = PsiTreeUtil.getChildrenOfTypeAsList(element, PsiClass.class);
 elements.addAll(classElements);
 for (PsiClass classElement : classElements) {
 //这里用了递归的方式获取内部类
 elements.addAll(getClasses(classElement));
 }
 return elements;
 }

格式化代码

 public static void reformatJavaFile(PsiElement theElement) {
 CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(theElement.getProject());
 try {
 codeStyleManager.reformat(theElement);
 } catch (Exception e) {
 LOGGER.error("reformat code failed", e);
 }
 }

使用粘贴板

 CopyPasteManager.getInstance()
 .setContents(new SimpleTransferable(table.toString(), DataFlavor.allHtmlFlavor));

更多

更多的技巧可以参考我的项目CodeMaker,以及其他的开源插件。

作者:风马萧萧