设计部分

因为平时在做项目的时候,总是会有一些重复代码的工作量,作为一个有追求的程序员,当然不会让自己一直重复这些劳动。于是,就有了IDEA插件开发这个方案了。IDEA插件开发的资料非常少,大部分都要阅读IDEA的源码来探索。

首先明确我的目标:根据模块和页面名称,自动初始化一系列的页面,其中的变量、类名、文件名等均根据模块名称和页面名称生成。

第二步就是设计交互,我初步计划是,模块和页面的文件夹由自己手工创建,在页面文件夹上右键,点击生成,生成文件夹里面的内容。

创建项目

新建一个IntelliJ Platform Plugin,写好项目名称,进入开发界面。

首先稍微介绍下目录结构。
1. resources目录,资源或者配置放置的目录,在META-INF中防止了插件的配置,以后模板文件也会放置在resource目录。
2. src中放置代码。
3. 其他如.ideaout等与编码无关,是IDE配置和编译输出文件等。

了解了目录结构,就要开始实现第一个目标。

实现菜单项

在IDEA中,每个动作都是一个action,所以第一步需要创建一个action。

在我的版本的IDEA中,创建的目录在这个位置,不同版本的IDEA位置可能不同,但是都大同小异,应该都叫做Action。
其中要填几个字段:
1. Action ID,也就是id了,保证唯一就可以了,随便怎么填,比如: My.newPage
2. Class Name,会为你创建的类的名称,如NewPageAction。
3. Name,展示的名称,如New Page Init。
4. Description,描述,鼠标移动到菜单上的时候,底部栏对此菜单的描述,随便写一下就可以了。
5. Add To Group,这是最重要的一栏,首先选Group,这是代表选项出现在哪一类菜单中,由于我是要让我的菜单出现在项目文件夹的右键菜单中,所以选择的是ProjectViewPopupMenu,旁边的Actions栏作用是和Anchor一起的,表示选项出现在现有选项的前面还是后面。我的选项出现在最后就可以了,所以不用选择Actions,直接在Anchor栏选择Last。
6. Keyboard Shortcuts,快捷键,我不需要,所以不设置了。

在创建完之后,会自动在META-INFplugin.xml中,创建一段标签:


    

并且会在src目录我们指定的包名中创建一个继承自AnAction的类,其中实现了actionPerformed方法,这个方法代表选项被点击的时候触发的方法。

获取需要的数据

首先介绍下我的插件,我的项目结构如下:

模块文件夹 ->
    页面文件夹 ->
        action文件夹
            action文件
            actionType文件
        reducer文件夹
            reducer文件
        component文件夹
            组件文件
        主页面文件

所以我的插件目标是,创建好模块文件夹和页面文件夹之后,在页面文件夹上点击右键,选择New Page Init,就可以生成下面的所有文件夹和文件。
那么第一步,就是需要获取到文件夹的名称了。

final DataContext dataContext = e.getDataContext(); //通过事件获取到当前的上下文
final IdeView view = LangDataKeys.IDE_VIEW.getData(dataContext); //获取到当前IDE的对象
if (view == null) {
    return;
}
final PsiDirectory dir = view.getOrChooseDirectory(); // 拿到当前选择的文件夹,也就是右键点击的文件夹

final Project project = PlatformDataKeys.PROJECT.getData(dataContext); // 拿到项目对象

if (dir == null || project == null) return;

到此,我们就拿到了需要的最基本的东西,准备工作做好了,因为页面中可能会用到各种各样的名称形式,所以现在来对基础数据做一些处理:

String[] split = dir.getName().split("-"); // 因为项目的约定,统一使用-来间隔单词
String dashName = dir.getName(); // 带-的名称,如my-page
String underscoreName = StringUtil.join(split, "_"); // 带下划线的名称,如my_page
String firstUpperCamelCaseName = Arrays.stream(split).map((w) -> Character.toUpperCase(w.charAt(0)) + w.substring(1).toLowerCase()).collect(Collectors.joining()); //所有首字母都大写的驼峰名称,如MyPage
String allUpperCaseUnderscoreName = underscoreName.toUpperCase(); // 带下划线的全大写名称,如MY_PAGE
String moduleName = "error"; // 模块名称,稍后处理
String moduleNameCamelCase = "error"; // 驼峰模块名称,稍后处理
String moduleUnderscoreName = "error"; // 模块下划线名称,稍后处理

PsiDirectory dirParent = dir.getParent(); // 获取page文件夹的上一层,也就是模块文件夹
if (dirParent != null) {
    moduleName = dirParent.getName(); // 模块名称
    if (dirParent.getName().contains("-")) {
        String[] parts = dirParent.getName().split("-");
        StringBuilder camelCaseString = new StringBuilder();
        for (int i = 0; i < parts.length; i++) {
            String part = parts[i];
            if (i != 0) {
                camelCaseString.append(part.substring(0, 1).toUpperCase()).append(part.substring(1).toLowerCase());
            } else {
                camelCaseString.append(part);
            }
        }
        moduleNameCamelCase = camelCaseString.toString(); // 驼峰模块名
    } else {
        moduleNameCamelCase = dirParent.getName();
    }
    moduleUnderscoreName = StringUtil.join(moduleName.split("-"), "_"); // 下划线模块名
}

基础数据就准备好了,可以进入下一步了。

页面模板

IDEA中,因为内置了Velocity,所以允许我们定义一些模板,首先先在resource中,创建一个文件夹,用于存放模板文件,名称叫做fileTemplates.internal,注意必须使用这个名称。
在下面新建所需要的模板文件,带上拓展名,并且以.ft结尾,如actions.tsx.ft
文件的语法与Velocity相同,把最基础的代码复制过来之后,将要替换的部分,使用Velocity的模板语法替换,就完成模板的编写了。
编写好了模板,还需要在plugin.xml中进行一些配置,让系统能够读取到相应的模板。
找到extensions标签,在标签下加入如下标签:


其中name为不带文件拓展名的名称,如actions.tsx.ft,就填入actions。这样在以后使用的时候,IDEA就会将模板名称与文件对应起来。

模板编写好了之后,回到原来的Action代码中,现在要进行的步骤是获取模板文件,并将值传入模板文件中,需要用到FileTemplateManager这个类了。

FileTemplateManager.getInstance(project).getInternalTemplate("actions"); // 获取actions模板

在拿到模板对象之后,还需要传入之前准备好的一些值:

Properties properties = new Properties();
properties.put("dashName", dashName);
properties.put("moduleName", moduleName);
properties.put("moduleNameCamelCase", moduleNameCamelCase);
properties.put("UpperPageName", firstUpperCamelCaseName);
properties.put("underscore_name", underscoreName);
properties.put("pageTitle", "请定义标题");
properties.put("allUpperCaseUnderscoreName", allUpperCaseUnderscoreName);

现在,就可以开始传入值了:

PsiDirectory actionsDirectory = dir.createSubdirectory("actions"); // 先在page目录下,创建一个目录actions
FileTemplate actionTemplate = FileTemplateManager.getInstance(project).getInternalTemplate("actions"); // 获取到actions的模板
try {
    FileTemplateUtil.createFromTemplate(actionTemplate, underscoreName + ".actions.tsx", properties, actionsDirectory); // 使用FileTemplateUtil的工具方法,在对应目录下,生成文件
} catch (Exception e1) {
    e1.printStackTrace();
}

到此,好像一个完整的编码流程就完成,那么运行一下吧。你会发现,运行的时候报错了。因为IDEA将写入有关的操作,都要放到另外的一个线程中执行,所以,需要告诉IDEA要执行的操作是一个写操作。在代码外面包上一层:

ApplicationManager.getApplication().runWriteAction(() -> {
    // .......我们的代码
});

这样,IDEA就会在写入操作的线程中执行我们的代码了。

测试并生成插件

开始运行的时候,需要配置一个运行环境,也就是启动另外一个IDEA,你可以去网上下载社区版本的IDEA来进行测试。如果以后需要编写到特殊的IDEA插件,比如TypeScript相关或者框架相关的插件,那么也可以配置付费版的IDEA或者Webstorm之类的来做测试。
在运行之前,最好还要先指定一下IDEA的最低版本,在plugin.xml中,有一段被注释的代码:


            《------就是它

打开这个注释,就代表IDEA会支持这个版本的api,具体版本查询,可以在上面的那行注释链接中去找。

到此应该能够将代码跑起来了,并且能够预览到插件的效果了,如果还有小bug,IDEA的调试是很方便的。接下来就是生成正式的插件了。
生成插件只需要点击菜单上Build -> Prepare Plugin Module ‘xxx’ For Deployment,就可以生成出对应的jar包了。

结语

有些同学可能说,这些功能不写IDEA的plugin也能写啊。确实,功能怎么样都能实现,只是看怎么样的实现最优美。在IDEA中点击右键,选择生成页面,这就是我觉得最优美的实现,所以才这么折腾在IDEA中实现这个功能。
并且,这个功能不是这个插件所有的功能,后续要做的功能,应该只有在IDEA中实现才最轻松,比如:解析reducer树并展示在IDE中;生成页面的时候可选页面初始化react组件(类似android)等等。这些功能都会依赖到js的语法解析,在IDEA中,psi直接可以访问到解析好的语法数据,后续我应该也会出更高级的IDEA插件教学。


1 条评论

开发者头条 · 2018年3月2日 10:04

感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/t3o7lj 欢迎点赞支持!
欢迎订阅《特约演员。的独家号》https://toutiao.io/subjects/10803

发表评论

电子邮件地址不会被公开。 必填项已用*标注