为了更好的提升团队的开发效率,一直都希望能够将一些团队内的重复工作以自动化的方式来优化。 因此希望基于Intellij的能力,来做一些提升解放劳动力的事情。

IDE插件开发简单介绍

就简单介绍一下常用的几个API,如果需要看详细的,可以看看官方的Intellij开发文档: plugins.jetbrains.com/docs/intell…

我们开发一个插件的流程可以分为下面几步:

  • 设置开发环境: 开发环境详细配置流程
  • 创建插件项目: 创建插件项目详细配置流程
  • 开发、运行和调试插件: 开发、运行和调试插件流程
  • 部署插件: 部署插件详细配置流程
  • 用户界面开发

    idea相关的页面开发主要还是使用swing组件。 idea内部包含很多自定义的swing的组件,我们开始时可以直接使用。 intellij用户界面组件

    Idea插件开发中,插件的操作被抽象成AnAction。通过AnAction可以处理用户点击行为,展示UI等。然后在plugins.xml注册Action,可以控制这个Action展示在 IDE UI 中的哪个位置。 当我们完成AnAction的定义和在plugin.xml注册后,就能接收到来自 IntelliJ 平台的用户行为回调了。

    public class AddTemplateModule extends AnAction {
        public static Project project;
        @Override
        public void actionPerformed(@NotNull AnActionEvent anActionEvent) {
            this.project = (anActionEvent.getData(PlatformDataKeys.PROJECT));
            initChooseUI();
            anActionEvent.getProject().getBaseDir().refresh(false, true);
    

    模版工程 & 模版Module

    在项目迭代过程中,在每一个新组件的开发总是避免不了需要创建不同代码仓库或者不同的Module。新工程和新模块的配置对于各个研发来说,并不是一件容易的事情,因为每个公司的项目都有一些个性化的配置,导致耗费很多时间在环境配置上。 所以迫切需要有一个快速提供工程创建、模块创建的能力。 所以对于这个问题,可以通过IDEA插件,自动化的创建一些模版工程、模版模块。在模版供工程和模版模块创建后,并且添加上自己公司的一些个性化配置项目,可以很大程度上的解决工程配置复杂和难的问题。

    模版Module创建

    先看看模版模块创建的一个demo。

    不管是创建模版工程还是模版模块,整体的实现步骤都大同小异。 简单来说可以分为下面几个阶段:

  • 创建模版工程/模版Module
  • 上传模版的压缩zip到指定远端地址
  • 在插件中将远端模版下载本地,并解压
  • 替换模版工程中的需要被替换的元素
  • 创建菜单选项

    要创建菜单选项,需要先创建AnAction的实现类。

    public class AddTemplateModule extends AnAction {
        public static Project project;
        @Override
        public void actionPerformed(@NotNull AnActionEvent anActionEvent) {
            this.project = (anActionEvent.getData(PlatformDataKeys.PROJECT));
            initChooseUI();
            anActionEvent.getProject().getBaseDir().refresh(false, true);
    

    然后在plugin.xml中配置展示期望展示的位置。

     <group id="KKPlugin.firstPlugin" text="kuaikan" description="kuaikan-plugin">
          <add-to-group group-id="MainMenu" anchor="last"/>
          <action class="plugin.KuaiKanAddTemplateModule" id="plugin.KuaiKanAddTemplateModule" text="创建快看模版模块" description="A test menu item"/>
          <action class="plugin.KuaikanAddTemplateProject" id="plugin.KuaikanAddTemplateProject" text="创建快看模版工程" description="A test menu item"/>
        </group>
    
  • id: 表示当前插件的唯一id
  • add-to-group: 表示当前要添加到哪个item上。 MainMenu:表示当前需要添加到主菜单上 anchor:表示当前当前展示的位置,last表示展示在最后
  • text: 表示当前展示的item的名称
  • action: class表示注册的anAction实现。
  • 接下来看看创建模版工程的主逻辑

    public void createModule(String libraryName, String uploadAarName, String targetSdkVersion) throws Exception {
             * step1 解压模版工程
            InputStream in = getClass().getResourceAsStream("/template/LibraryTemplate.zip");
            String zipFileAbsPath = project.getBasePath() + "/LibraryTemplate.zip";
            FileOutputStream fo = new FileOutputStream(zipFileAbsPath);
            IOUtils.copyStream(in, fo);
            UnZipUtils.unZip(zipFileAbsPath, project.getBasePath());
            FileUtils.deleteFile(zipFileAbsPath);
            File templateFile = new File(project.getBasePath() + "/LibraryTemplate");
            String destFileName = project.getBasePath() + "/" + libraryName;
            templateFile.renameTo(new File(destFileName));
             * step2 修改包相关路径
            File currentPackageName = new File(destFileName + "/src/main/java/com/kuaikan/library/template");
            String packageDestFileName = destFileName + "/src/main/java/com/kuaikan/library/" + libraryName;
            currentPackageName.renameTo(new File(packageDestFileName));
             * step3 修改AndroidManifest包名
            ReplaceFile.replaceFileStr(destFileName + "/src/main/AndroidManifest.xml", "com.kuaikan.library.template", "com.kuaikan.library." + libraryName);
             * step4 增加到setting.gradle中
            String settingGradleFileName = project.getBasePath() + "/settings.gradle";
            FileUtils.appendString2File(settingGradleFileName, "include ':" + libraryName + "'" );
             * step5 替换build.gradle文件
            ReplaceFile.replaceFileStr(destFileName + "/build.gradle", "template", uploadAarName);
    

    创建模版工程和模版Module的主逻辑基本一致。

    创建模版代码

    对于模版代码,ide中已经给我们主动生成了一些模版代码生成器,比如说set、get方法。每一家公司都会有自己的一些公用代码,类似这种类型的代码,如果通过IDEA插件来做一些自动化的生成,那么也能够更好的提升团队的开发效率。

    先看看模版代码创建的一个demo。

    demo中有两个不同的生成代码的例子。

  • 第一个跟上面的模版工程和模版Module的生成方式类似,生成部分模版文件。
  • 第二个可以自定义生成的代码。在已有文件中进行模版代码的插入。
  • 创建模版代码

    这个插件创建的模版代码就是之前文章介绍过的页面框架,有兴趣的可以阅读一下:快看Android页面框架

    创建菜单选项

    当前的菜单选项添加到创建的面板中,

          <action id="KKArchGenerateAction" class="plugin.arch.KKArchGenerateAction" text="archClass"
                  description="arch代码生成器" icon="AllIcons.Actions.Colors">
              <add-to-group group-id="NewGroup" anchor="first"/>
              <keyboard-shortcut keymap="$default" first-keystroke="shift meta G"/>
          </action>
    
  • NewGroup:创建文件时的New分组。
  • keyboard-shortcut:表示当前配置快捷键。
  • 创建模版代码的主体逻辑

    public void generateArchCode(String name, int type) {
             * step1 解压模版文件到指定目录中
            String zipFileAbsPath = project.getBasePath() + "/LibraryTemplate.zip";
            FileOutputStream fo = null;
            //ide布局相关的可以从IDEView中获取,包括目录
            IdeView ideView = (IdeView)anActionEvent.getRequiredData(LangDataKeys.IDE_VIEW);
            PsiDirectory directory = ideView.getOrChooseDirectory();
            if (directory == null) {
                return;
            //解压缩指定目录
            String destPath = directory.getVirtualFile().getPath();
            UnZipUtils.unZip(zipFileAbsPath, destPath);
            FileUtils.deleteFile(zipFileAbsPath);
             * 替换包名
            String packageName = getFilePackageName(directory.getVirtualFile());
            String mainFileName = destPath + "/"+  (type == TYPE_ACTIVITY ? "ArchTemplateActivity" : "ArchTemplateFragment") +".kt";
            ReplaceFile.replaceFileStr(mainFileName, PACKAGE_TEMPLATE, packageName);
             * 替换所有文件类名
             * 1.ArchTemplateActivity | ArchTemplateController | ArchTemplateDataProvider | ArchTemplateMainModule
            ReplaceFile.replaceFileStr(mainFileName, "ArchTemplate", name);
             * 替换成员名称
            ReplaceFile.replaceFileStr(mainFileName, "archTemplate", captureName(name));
             * 替换文件名
            String destMainFileName = destPath + "/"+ name +  (type == TYPE_ACTIVITY ? "Activity" : "Fragment") +".kt";
            FileUtils.reNameFile(mainFileName, destMainFileName);
    

    这块逻辑中比较关键的一个点是如何获取到当前文件面板中选中的路径。 可以通过IDE_VIEW获取当前选中的目录。

    IdeView ideView = (IdeView)anActionEvent.getRequiredData(LangDataKeys.IDE_VIEW);
    PsiDirectory directory = ideView.getOrChooseDirectory();
    

    第二个自动配置生成代码,会在下面的KK字典中一起介绍。

    所谓的KK字典,就是将快看的配置文档,代码生成,聚合到统一文档列表中,根据选择的选择的文档,进行文档的查看和代码生成。

    创建搜索面板

    这个搜索面板借鉴了已有的插件实现: exynap

    先看一下想要实现的效果:

    沉浸式阅读

    不知道大家对于文档的编写和阅读的流程是怎么样的,但是或多或少会存在下面几个问题:

  • 文档的编写都是一次性的,就是说在首次编写完设计和使用文档之后,后续有一些原始功能的改动,很少会同步更新到文档中,导致文档跟当前的设计不一致
  • 对于文档的搜索,首先不知道当前存在着哪些文档,其次是很难搜索到我们想要内容
  • 文档的编辑和代码管理是分离,不好维护
  • 针对上面几个问题,我们期望能够做一套沉浸式的文档阅读开发。 对于文档的编写、更新、维护、阅读都能够在同一个工具中实现。借助于IDEA插件,

    所有研发都可在Android studio中发布和更新文档,当文档发生变更时,其他研发在在Android Studio中阅读到最新的文档。

    先看看整体的沉浸式阅读的架构设计

    通过这个设计图,我们可以再简单了解下整个流程的扭转过程:

  • 研发A和研发C通过发布组件更新远端aar,在组件发布的同时,执行处理引擎
  • 处理引擎会通过解析本地json,发现存在需要上传/更新文档A和文档C,将文档上传到远端的数据存储中,在我们的场景中,直接存储在了gitlab的仓库内。
  • 研发C通过插件进行搜索,可以直接查看到最新的文档A和文档C
  • 首先是文档上传模块。在组件发布使用我们自研的发布插件。 如果有兴趣了解发布插件,可以了解之前发布的文章:快看发布插件。 在组件发布时,如果当前时发布的时远端的release版本,就会通过处理引擎进行处理。

    在查看处理引擎的逻辑之前, 需要先介绍下文件上传的规范。

    本地Json定义

    在待发布工程的组件内,需要在本地的当前project的根目录下创建dict目录,配置guide.json文件. json文件的格式如下:

    "result": [ "id": "LibArch-ArchDesignGuide", "searchKey": "read the arch Design guide document", "type": 2, "group": "架构", "desc": "查看Arch框架的设计文档!", "mdFilePath": "guide/archdesign.md", "autoAddedCode": "xxx"

    看看各个参数的含义:

  • id:唯一id,作为存储的唯一ID
  • searchKey: 表示在搜索框中可以被搜索到的key
  • type:表示当前json配置的类型,type为2,表示类型是文档,type为1,表示类型时生成代码
  • desc:在聚合指导文档中,展示的简介
  • group:表示当前搜索的分组
  • mdFilePath: 如果当前类型是上传文档,这个参数就是文档的本地路径
  • autoAddedCode:如果当前类型是生成代码,这个参数就是需要生成的代码
  • 自行编写文档,将文档地址配置到上面的guide.json中。

    处理引擎执行的逻辑如下:

  • 自动解析本地配置的guide.json文件,与远端的聚合json进行比较
  • 当前文件属于新增或者更新,需要先把文件上传至gitlab上,同时更新远端聚合json,把新增的搜索项合并到聚合json中。
  • 统一的指导文档

    如果当前各个模块都各自上传了自己的文档,也有了自己的配置的对应的搜索的关键词。 但是存在的问题是现在研发并不知道当前配置了多少文档以及其对应的关键词。 所以需要另外生成一个指导文档,通过这个文档,可以查看到所有的配置的所有搜索词、描述。所以需要在上传时动态生成一个markdown文档。 也就是用上面的本地json配置。

    //当前搜索的分组 "group": "架构", "searchKey": "xxxxxx" "desc": "查看Arch框架的设计文档!"

    合并生成下面这样的markdown文件:

    ## 搜索关键词: 搜索关键词描述
    ### 分组:架构
    1. read the arch Design guide document : 查看Arch框架的设计文档!
    2. read the arch Use guide document : 查看Arch框架的使用文档!
    ### 分组:KK-UI
    1. read the kkDialog guide document : 查看KKDialog的使用文档!
    2. read the kkShadow guide document : 查看阴影组件的使用文档!
    3. read the kkLoadingView guide document : 查看Loading组件的使用文档!
    4. read the kkTips guide document : 查看Tips组件的使用文档!
    5. read the kkToast guide document : 查看Toast组件的使用文档!
    6. read the kkToolbar guide document : 查看Toolbar组件的使用文档!
    7. read the carousel guide document : 查看轮播组件的使用文档!
    8. generate the kkDialog code : 生成KKDialog模版代码!
    9. read the feedbackView guide document : 查看负反馈组件的使用文档!
    10. read the kkButton guide document : 查看Button组件的使用文档!
    11. read the kkPopup guide document : 查看Popup组件的使用文档!
    12. read the kkBottomMenu guide document : 查看底部菜单组件的使用文档!
    13. read the halfScreen guide document : 查看半屏弹窗组件的使用文档!
    ### 分组:KK-LibAssistanceTool
    1. read the remote Debug Tool guide document : 查看远程调试工具的使用文档!
    

    最后在所有搜索的关键词中,都能够搜索到这个文件。

    基于gitlab的文档上传

    使用gitlab的作为远端文件存储有一个比较重要的原因是能够节省服务端的人力。 gitlab的统一API已经比较完善了,我们完全可以基于gitlab的服务和数据存储,完成所有必须的操作。

    创建新文件:gitlab创建文件 更新文件:gitlab更新文件 删除文件:gitlab删除文件 下载文件:gitlab下载文件

    远端文件下载并阅读

    需要先获取当前的聚合json。 这样才能知道当前搜索面板中共有多少可搜索的内容。 聚合json的内容格式如下:

    "result":[ "id":"LibUi-kkDialogGuide", "searchKey":"read the kkDialog guide document", "group":"KK-UI", "desc":"查看KKDialog的使用文档!", "mdFileUrl":"https://git.quickcan.com/api/xxxxx/repository/files/guide%2Fkkdialog.md/raw?ref\u003dmaster", "type":2

    这个跟本地上传的文件json类型和类似,唯一的不同的点是mdFileUrl不再是本地的markdown文件的路径,需要替换成远端文件的url地址。 gitlab文件存储的地址可以转化为固定的下载地址。

    在选中了某个item后,使用gitlab下载文件api,将gitlab的文件下载到本地。

    val file = File(project.basePath)
    val parentFileAbsPath = file.parentFile.absolutePath
    val outPutFileName = selectItem.guideMdUrl.hashCode().toString() + ".md"
    val outputFilepath = "$parentFileAbsPath/.md/$outPutFileName"
    RestClient.loadMdFile(selectItem.guideMdUrl!!, outputFilepath)
    

    当然,下载需要下载到本地的公有目录中,而不是当前的git目录中,否则就会把下载下来的markdown当成修改的文件。 在文件下载成功之后,需要自动打开的下载成功的markdown文件。

    val needToOpenFile = File(outputFilepath)
    val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(needToOpenFile)
    OpenFileDescriptor(project, virtualFile!!).navigate(true)
    
    markdown阅读配置

    不管时文档编辑还是文档阅读,使用markdown还是比较方便的。 所以需要在IDE上支持markdown的编辑和阅读。 在Android Studio上,对于markdown的支持并不是太好。 需要有一些特殊的配置。 Androidstudio配置mardonw的流程。

    目前新版Android studio对于markdown的适配有点问题,需要我们单独去进行配置。需要按照下面的流程配置。

  • 下载对应的markdown插件,idea本身提供的markdown插件或者个人开发者开发的mardownEditor插件
  • 安装JBR(confluence.jetbrains.com/display/JBR…
  • 通过本地安装 Choose Runtime插件。ChooseRuntime-1.2.zip
  • 在Help > Find Action,输入Choose Runtime,然后选择第一步解压的地址即可。
  • 自定义代码生成

    type定义为2,表示文档的上传。type为1,表示自动生成代码。 如下图所示:

    "id":"LibUi-kkDialogGenerate", "searchKey":"generate the kkDialog code", "group":"KK-UI", "desc":"生成KKDialog模版代码!", "type":1, "autoAddedCode":"KKDialog.Builder(needContext)\n.setTitle(\"title\")\n .setMessage(\"message\")\n.setCancelable(false)\n .setCloseOnTouchOutside(false)\n.setNegativeButton(\"NegativeButtonName\",\n object : KKDialog.OnClickListener {\n override fun onClick(dialog: KKDialog, view: View) {\n \n }\n} )\n .setPositiveButton(\"positiveButtonName\",\nobject : KKDialog.OnClickListener {\n override fun onClick(dialog: KKDialog, view: View) {\n\n}\n })\n.show()"

    当选中了这个Item之后,会将配置的代码插入到光标展示的位置中。

    val caret = event.getData(PlatformDataKeys.CARET) //当前编辑器 val editor = event.getData(PlatformDataKeys.EDITOR_EVEN_IF_INACTIVE) //当前文件 val document = editor?.document document.insertString( caret.offset, autoAddedCodeString

    通过获取光标和文件Editor,可以进行文件的变更。

    Intellij插件能够实现的功能远远不止这些, 后续希望能够扩展更多的能力。

  • 在代码联想功能中添加快看的自动代码生成
  • 基于插件提供的PSI能力,做一些通用的静态代码分析,将代码问题暴露的更前头。(目前java的PSI能力是比较完善了,Kotlin的PSI能力有待优化)
  • 分类:
    开发工具