🤖 自定义IDEA插件 → 划词翻译替换(上)

7,242 阅读9分钟

本文正在参加「金石计划」

在上上节 《花3分钟,重拾开发效率神器 → Live Templates》 中,杰哥整活写了个 自定义中文转英文的Expression。以为从此就可以告别编写布局xml时为View控件设置id的繁冗操作:

打开翻译软件 → 输入控件名称 → 翻译 → Copy到xml中 → 单词字母全转换为小写 → 添加下划线

但在实际开发使用中,虽然能用,但存在下述问题:

  • 轻微卡顿:机制原因,每个文字都会触发一次转换,多个同步请求导致响应较慢。
  • 不支持连续输入中文:输入完中文,自动转换后,再次输入,焦点会跳到外面进行输入。
  • 没定义模板的View不支持:得覆盖一堆不常用的View,谁顶得住啊?

综上,Live Template并不能满足我日常的开发需求,需要另辟蹊径来补齐这些短板。其实,我要的并不多:

只是希望 中文能自动转换成英文 而已~

在使用IDEA插件 Translation 划词翻译时,我突然灵机一动,完全可以自己写个类似的插件:

  • 1、选中要翻译的文本
  • 2、右键(或快捷键)
  • 3、对选中文本进行翻译
  • 4、处理翻译结果
  • 5、替换选中的文本

看着步骤有点多,其实345是自动完成的,话不多说,直接动手实现一波~


0x1、插件开发初体验

先按照网上烂大街的基础教程,搭个环境,写个Demo跑起来,然后再自定义。不过上来就遇到一个大坑...

① 大坑:JDK版本问题

不建议用Android Studio开发IDEA插件,得装一堆乱七八糟的库,直接用IDEA社区版(免费版) 也支持IDEA插件开发。笔者用的最新的IDEA,但在新建项目时并没有找到:InteliJ Platform Plugin,只找到一个 IDE Plugin

感觉做了功能合并啥的,还支持Kotlin,尝尝鲜?点下OK却提示:

擦,需要Java 11,em...前阵子群里有人说一些插件升级后,就用不了,估摸着跟这个有关系吧。杰哥还在用Java 8,妥妥是不能上的,只能下一个旧一点的IDEA版本了。

随手下了个 2021.3.3 的版本,新建项目后,编译报错,即便我手动指定了低版本的Gradle也于事无补:

报错原因:IDEA、gradle-intellij-plugin(Gradle插件)、gradle、JDK 有版本对应关系,不匹配就GG了。先通过 IDEA版本 → 找到Gradle版本Third-Party Software and Licenses

选择当前版本,比如我的2021.3,然后页面搜 Gradle

用的Gradle 7.1,从7.0开始要求 最低要求的JDK版本为JDK 11,解法有两:

解法1:修改IDEA的Gradle JVM版本为11+

解法2:换使用Gradle 6.x的IDEA

笔者采用的解法2,最终定位到 2021.1.3 采用Gradle 6.8:

打开官网 Other Versions 直接定位到 2021.1.3的社区版 下载安装。

新建项目后依然报错,默认采用最新的 org.jetbrains.intellij 需要手动指定下版本。

可以看到发行日期应该是2021.6.30,在 mvnrepository 搜 org.jetbrains.intellij 定位到大概的时间点:

打开build.gradle把版本号改为1.1:

然后就可以编译通过,开始写代码,但当我编写好Action准备运行时,又翻车了

问下Bing AI:

点开 platform-api.jar包的 MANIFEST.MF 文件

我服了,看来想使用Java8还得使用更旧的IDEA,又换成了 2020.2.4 的版本,终于可以了!


② 创建插件工程

创建方式有两种:

这里选第二种:

点击OK,静待项目编译成功,看下 插件核心配置文件plugin.xml

不难看出它用于配置:插件名称作者信息插件介绍Action 等信息。

接着看下 项目依赖配置文件build.gradle

也不难看出它用于配置:第三方依赖插件版本插件版本更新记录 等信息。


③ 创建插件入口Action

Action是IDEA提供的 事件响应处理器,可以通过它来自定义一些事件处理逻辑/动作。

依次:右键java目录 (没有自己手动建)NewPlugin DevKitAction

按需填写Action的相关配置项:

点击OK,会生成一个Action类:

plugin.xml 也会自动生成此Action的配置信息:

接着随便加个弹窗的代码玩玩:


④ 编译运行

依次:点击右侧Gradle面板TasksintellijrunIde,然后会启动一个默认安装了这个插件的IDEA~

但报错了:编码GBK的不可映射字符 解决方法:build.gradle中增加utf-8的编码方式

tasks.withType(JavaCompile) {
    options.encoding ="UTF-8"
}

再次编译,如愿启动了一个新的IDEA,点击 菜单栏的Tools 可以看到我们定义的动作名称:

接着点击,会有一个弹窗:

o(╯□╰)o 才发现我把标题和内容参数传错了,Demo暂且体验到这,接着来写我们的功能插件了~


0x2、实现自定义插件

想了一下,我这种 翻译替换文本的需求 其实可以归类为 字符串格式化,那插件名称就叫 CpStringFormat 吧。

另外,除了 翻译结果转小写加下划线 的需求外,有时还需要 翻译结果单词首字母大写,索性就弄个 有子级的右键菜单吧,以后有啥常用的字符串格式化,直接加~

① 带子级的右键菜单

《自定义 idea 插件开发》 这篇文章里,看到了这个:

不难看出 group标签 能实现我们的需求,红框框住的也是三个要点,直接创建两个Action,简单配置下:

<actions>
    <action id="CnEnLowerULine" class="CnEnLowerULineAction" text="中-&gt;英 (小写+下划线)"
            description="中-&gt;英 (小写+下划线)">
        <!--  设置快捷键为ctrl+1  -->
        <keyboard-shortcut keymap="$default" first-keystroke="ctrl 1"/>
    </action>
    <action id="CnEnWord" class="CnEnWordAction" text="中-&gt;英(单词首字母大写)"
            description="中-&gt;英(单词首字母大写)">
        <!--  设置快捷键为ctrl+2  -->
        <keyboard-shortcut keymap="$default" first-keystroke="ctrl 2"/>
    </action>
    <group popup="true" id="StringFormatMenu" text="CpStringFormat" icon="/icons/icon.png">
        <reference ref="CnEnLowerULine"/>
        <!-- 设置分割线   -->
        <separator/>
        <reference ref="CnEnWord"/>
        <!-- 添加到右侧菜单栏,并置顶  -->
        <add-to-group group-id="EditorPopupMenu" anchor="first"/>
    </group>
</actions>

我这里还弄了一个 喵喵怪的logo (经多次测试,发现 16x16 的显示效果加载),运行runIde后右键看看效果:


② 获取选中文本

工具代码类代码与其满大街找,不如直接问ChatGPT:

不难看出 红框部分 就是获取选中文本的代码,加了个弹窗验证下,果然输出选中的文本~


③ 替换选中的文本

接着我又Copy了 黄框部分 代码尝试替换文本,但发现并 没有替换成功,有问了下ChatGPT:

提供的方案并不太行,后面在另一个开源项目:judasn/ChineseTypography-IDEA-Plugin 找到了解决方法:

再问ChatGPT:

给出的示例代码也和项目给出的代码如出一辙,最后还给出了一个建议:

当然,对这个建议持保留意见,2333,毕竟AI偶尔也会瞎编~


④ 调用百度翻译API

之前的翻译脚本是基于Groovy开发的,现在得转成Java代码,这种繁琐的工作,同样可以交给ChatGPT:

但转换后的代码引入了第三方库Jackson,插件的话,当然是尽可能不依赖第三方的,这样体积也会更小。

那就让ChatGPT不要引入第三方库:

可以看到,它使用到了最原始的 HttpURLConnection 发送请求,Scanner 类读取响应文本。

接着就是CV它给出的代码,修修剪剪 + 拆类 + 调试,得出真正的可用代码,先是 翻译的工具类 (TranslateUtil.java):

import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Formatter;
import java.util.Scanner;

public class TranslateUtil {
    private static final String appId = "xxx"; // 替换成自己的 App ID
    private static final String appKey = "xxx"; // 替换成自己的 App Key
    private static final String from = "zh"; // 原始语言
    private static final String to = "en";   // 转换后的语言

    static String fetchTranslateResult(String query) {
        // 转换后的结果
        String translation = null;
        try {
            // 构造salt参数
            String salt = String.valueOf(System.currentTimeMillis());
            // 构造Sign参数
            String sign = generateMD5(appId + query + salt + appKey);
            // 拼接请求URL
            String url = "http://api.fanyi.baidu.com/api/trans/vip/translate" +
                    "?q=" + URLEncoder.encode(query, "UTF-8") +
                    "&from=" + from +
                    "&to=" + to +
                    "&appid=" + appId +
                    "&salt=" + salt +
                    "&sign=" + sign;
            // 响应数据
            String responseText = "";
            HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
            connection.setRequestMethod("GET");
            connection.setConnectTimeout(5000);
            connection.setReadTimeout(5000);
            connection.connect();
            int responseCode = connection.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_OK) {
                Scanner scanner = new Scanner(connection.getInputStream(), "UTF-8");
                responseText = scanner.useDelimiter("\\A").next();
                scanner.close();
            }
            connection.disconnect();

            if (responseText.contains("\"error_code\":")) {
                translation = responseText;
            } else {
                // 解析翻译结果
                int startIndex = responseText.indexOf("\"dst\":\"") + 7;
                int endIndex = responseText.indexOf("\"", startIndex);
                translation = responseText.substring(startIndex, endIndex);

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return translation;
    }

    // 生成MD5
    public static String generateMD5(String string) {
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            byte[] hashBytes = messageDigest.digest(string.getBytes(StandardCharsets.UTF_8));
            Formatter formatter = new Formatter();
            for (byte b : hashBytes) {
                formatter.format("%02x", b);
            }
            String hash = formatter.toString();
            formatter.close();
            return hash;
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }
}

然后是 翻译转小写然后拼接下划线的类 (CnEnLowerULineAction.java):

public class CnEnLowerULineAction extends AnAction {
    @Override
    public void actionPerformed(AnActionEvent e) {
        Editor editor = e.getData(CommonDataKeys.EDITOR);
        if (editor == null) return;
        SelectionModel selectionModel = editor.getSelectionModel();
        String selectedText = selectionModel.getSelectedText();
        if (selectedText == null) return;
        Runnable writeAction = () -> {
            String translation = TranslateUtil.fetchTranslateResult(selectedText).toLowerCase()
                    .replaceAll(" ", "_").replace(",", "");
            editor.getDocument().replaceString(selectionModel.getSelectionStart(), selectionModel.getSelectionEnd(), translation);
        };
        WriteCommandAction.runWriteCommandAction(editor.getProject(), writeAction);
    }
}

最后是 翻译后单词首字母大写拼接的类 (CnEnWordAction.java):

import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.SelectionModel;

public class CnEnWordAction extends AnAction {
    @Override
    public void actionPerformed(AnActionEvent e) {
        Editor editor = e.getData(CommonDataKeys.EDITOR);
        if (editor == null) return;
        SelectionModel selectionModel = editor.getSelectionModel();
        String selectedText = selectionModel.getSelectedText();
        if (selectedText == null) return;
        Runnable writeAction = () -> {
            // 根据空格分割成单词数组,遍历首字母大写拼接
            String[] words = TranslateUtil.fetchTranslateResult(selectedText).split(" ");
            if (words.length == 0) return;
            StringBuilder sb = new StringBuilder();
            for (String word : words) sb.append(word.substring(0, 1).toUpperCase()).append(word.substring(1));
            editor.getDocument().replaceString(selectionModel.getSelectionStart(), selectionModel.getSelectionEnd(),
                    sb.toString().replace(",", ""));
        };
        WriteCommandAction.runWriteCommandAction(editor.getProject(), writeAction);
    }
}

⑤ 看下效果

万事俱备,接着就是运行验证了,直接上图:

前两个通过右键选中进行转换,后面直接用快捷键 ctrl+1/2 快速转换,简直不要太爽!!!此处应有掌声:


0x3、小结

本节简单记录了杰哥开发 划词翻译替换插件 的过程,先是踩了个 JDK版本问题的大坑,然后抄网上的教程写了个 简单Demo跑起来,最后 借助ChatGPT快速完成了划词翻译替换功能。当看到插件用起来那一刻,还是成就感满满的~

因为用到了百度翻译的API,Key是写死的,不太好Share。下节来给它写个设置页,允许填写读者自己的ID和Key,顺带演示下如何到处插件包及上传至插件市场,敬请期待~