本文正在参加「金石计划」
在上上节 《花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目录 (没有自己手动建) → New → Plugin DevKit → Action:
按需填写Action的相关配置项:
点击OK,会生成一个Action类:
plugin.xml 也会自动生成此Action的配置信息:
接着随便加个弹窗的代码玩玩:
④ 编译运行
依次:点击右侧Gradle面板 → Tasks → intellij → runIde,然后会启动一个默认安装了这个插件的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="中->英 (小写+下划线)"
description="中->英 (小写+下划线)">
<!-- 设置快捷键为ctrl+1 -->
<keyboard-shortcut keymap="$default" first-keystroke="ctrl 1"/>
</action>
<action id="CnEnWord" class="CnEnWordAction" text="中->英(单词首字母大写)"
description="中->英(单词首字母大写)">
<!-- 设置快捷键为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,顺带演示下如何到处插件包及上传至插件市场,敬请期待~