引言
前段时间开发环境从笔记本换到台式机,为了能更好的偷懒,安装了很多AndroidStudio插件,其中有一款插件Exynap官方网站,效果十分让我动心。
但安装在我的Window上并没有任何的代码库被搜寻出来,让我一度怀疑我是不是因为被墙了的原因。最后在官网上,才看到了这样一句话:System Requirements (OSX only)
,简直是莫大的遗憾。不过Mac的同学,还是可以去试一下的:Exynap下载地址
好吧,看来利用现成的工具是不可能的了,虽然有Mac,但平时工作都是在Windows上,不患寡而患不均,就只能琢磨琢磨脑袋自己来实现这个让我十分心动的功能了。
于是仿Exynap的代码库IDEA插件v1.0 CodeTemplete
就新鲜出炉了,效果图如下:
初代1.0版本的缺陷还是很多的,先不说界面的友好性或者美观程度,单功能上还是远远比不上Exynap,先说实现的功能吧:
1. 可以根据关键字模糊搜索代码块,并插入到光标位置
2. 能够增改代码块及相关信息
3. 配置参数功能,目前支持单参数,选中被替换的参数后可以替换掉原代码块的变量
4. 本地库保存代码块,可通过复制数据库达到多平台自定制
目前还缺很多计划内的功能:
1. 界面拖拽及缩放
2. 代码描述展示(还没找到合适的位置展示到界面上)
3. 快速导包(代码块涉及的类无法自动导包)
4. 格式化代码(因为只简单的插入到光标位置,并没有正确缩进)
5. 删除功能和关键词去重
6. 插件库反复卸载无法保留本地库
7. 无法多人共享数据库
8. 文件存储模式(SQLite文本长度限制,所以可以采用最简单的文本存储来解决)
9. 多参数模式
10. 光标自定义位置跳转(可变参数输入确认)
……
还有很多,先上一版,后续,慢慢来吧~
开发准备
首先阅读一下官网资料,插件开发文档
虽然说已经能支撑起初步的开发了,但文档还并不是很全面,有一些细节化的内容并没有串起来,也没有对应的API说明,这个着实有点难办。(也可能是阅读不仔细,没有领会的原因。)
AndroidStudio的插件开发需要在Intelij上进行开发,首先我们新建工程:
接下来我们需要了解一个只是,插件开发其实是一个动作触发的过程,我们要对这个动作触发事件进行一定的配置,比如名称(id),触发类(Java Class),所在菜单,对应快捷键等等,我们可以在resources/META-INF/plugin.xml
里面手动更改,也可以通过右键new一个Action
的方式直接新增一个插件事件:
界面上的没过多的配置项,简单附上官网的说明:
Action ID - Every action must have a unique ID. If the action class is used in only one place in the IDE UI, then the class FQN is a good default for the ID. Using the action class in multiple places requires mangling the ID, such as adding a suffix to the FQN, for each ID.
Class Name - The FQN implementation class for the action. If the same action is used in multiple places in the IDE UI, the implementation FQN can be reused with a different Action ID.
Name - The text to appear in the menu.
Description - Hint text to be displayed.
Add to Group - The action group - menu or toolbar - to which the action is added. Clicking in the list of groups and typing invokes a search, such as “ToolsMenu.”
Anchor - Where the menu action should be placed in the Tools menu relative to the other actions in that menu.
在里面所配置的相关信息,也会以配置文件的方式记录在resources/META-INF/plugin.xml
中。
新建的Action
类中,有一个被默认携带的方法actionPerformed
,携带参数AnActionEvent
:
AnActionEvent
可以说是获取当前状态的重要入口了,通过查看官网给的信息,我们可以获取到诸如工程对象、文件对象、光标状态等等的信息,没有在官网找到更详尽的解释,我搜寻了一下,找到了一个收录了比较有用的代码实例的网站,这里也顺便贴出来一些比较常用的:
@Override
public void actionPerformed(AnActionEvent e) {
Project project = e.getData(PlatformDataKeys.PROJECT);
project = e.getData(PlatformDataKeys.PROJECT);
VirtualFile[] vFiles = e.getData(PlatformDataKeys.VIRTUAL_FILE_ARRAY);
e.getPresentation().setVisible(ApplicationManager.getApplication().isInternal());
Navigatable nav = e.getData(CommonDataKeys.NAVIGATABLE);
Editor editor = e.getData(CommonDataKeys.EDITOR);
PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE);
FileEditorManager fileEditorManager = FileEditorManager.getInstance(project);
Module[] modules = ModuleManager.getInstance(mProject).getModules();
}
毕竟了解的还不是很深,我也就不写自己的理解了,后续的版本优化基本上是离不开这些信息的,以后用到了再浅谈。
除了actionPerformed
方法,自定义的AnAction
类中还有一个方法也值得我们注意:update
,这个方法其实可以用来适配场景控制插件的可见可用性等等,生命周期update
在前。
@Override
public void update(AnActionEvent e) {
}
讲完了这些内容之后,我们可以初步动手实践一下,按照官网给的代码示例,动手实践一下,
效果显而易见,当然,这里测试是否成功的方法有两种:
-
通过
Run/Debug Configurations
配置Plugin
的运行模式,通过IDEA调起一个新的IDEA窗口测试检查 -
通过菜单栏
Build
->Prepare Plugin Module 'XXX' For Deployment
构建插件输出包,通过本地手动安装插件包来进行测试,当然每更改一次代码都需要重新编译再重启运行环境下的编译器。
构建需求
设计的思路不再赘述了,就是仿Exynap
这个插件……的缩减版,首先能用到基础功能,后续慢慢添加,这里肯定涉及数据库的增删改查,以及工程文件的操作,以及Swing UI的开发。
数据库
这个插件开发的本意是在AndroidStudio上运行的,所以我第一时间想到的数据库是SQLite,而且十分轻量级,能够满足需求。当然,插件开发出来,也可以在Intelij上使用。
和Android开发不同的是,SQLite的连接使用的是JDBC的方式,两种方式,但我们采用了第二种:
try {
Class.forName("org.sqlite.JDBC");
DriverManager.getConnection("jdbc:sqlite:xx.db");
mConnection = ds.getConnection();
} catch (Exception e){
e.printStackTrace ( );
}
try {
SQLiteDataSource ds = new SQLiteDataSource();
ds.setUrl("jdbc:sqlite:xx.db");
mConnection = ds.getConnection();
} catch (Exception e){
e.printStackTrace ( );
}
SQLite的好处就是当该地址不存在SQLite数据库文件的时候,会自动创建数据库文件。为了规范化插件的数据库管理,我把数据库的地址设置在了本插件的安装目录。
一般来说,从本地安装插件的目录就在C:\Users\Administrator\.IntelliJIdeaxxxx.x\config\plugins
下面,直接调试运行时获取的目录为:C:\Users\Administrator\.IntelliJIdeaxxxx.x\system\plugins-sandbox\plugins
,但环境不同,我们也获取的路径不同,不过后期为了解决数据库的迁移和防卸载删除问题,肯定是要用户自定义数据库位置的,目前我们默认插件安装位置:
private String getDBURL() {
// 获取配置信息
Properties properties = System.getProperties();
String url;
// 获取用户自定义的插件路径
url = properties.getProperty("idea.plugins.path");
if (null == url || 0 == url.replace(" ", "").length()){
// 如果不存在用户自定义路径则为默认路径,一般为C:\Users\Administrator\.IntelliJIdeaxxxx.x\config\plugins
// 需要获取user.home目录去拼接
url = properties.getProperty("idea.config.path");
if (null == url || 0 == url.replace(" ", "").length()){
url = properties.getProperty("user.home");
url += "\\.";
url += properties.getProperty("idea.paths.selector");
url += "\\config\\plugins";
} else {
url += "\\plugins";
}
}
url = "jdbc:sqlite:" + url + "\\CodeTemplatePlugin\\templete.db";
return url;
}
mConnection
就是我们获取的连接句柄了。
接下来就是惯用的版本问题了,其他升级的可以不考虑,但数据库版本升级是非常有必要的,因此我们也需要来一套Android的数据库版本升级的方式。
获取当前版本的语句是PRAGMA USER_VERSION
,像执行SELECT语句一样执行后就可以获取,默认是0,我们初代版本就设置成1就好了,同时创建Table;
设置版本的语句是PRAGMA USER_VERSION = 1
,同样执行语句就能完成版本号的替换。
上代码:
private checkVersion(){
int curVersion = getVersion();
if (curVersion < VERSION){
updateVersion(curVersion);
}
}
private int getVersion(){
int version = 0;
try {
PreparedStatement preparedStatement = mConnection.prepareStatement("PRAGMA USER_VERSION");
ResultSet resultSet = preparedStatement.executeQuery();
if (resultSet.next()){
version = resultSet.getInt("USER_VERSION");
}
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
return version;
}
private void updateVersion(int oldVersion){
switch (oldVersion){
case 0:
createTable();
break;
}
try {
PreparedStatement preparedStatement = mConnection.prepareStatement("PRAGMA USER_VERSION = " + VERSION);
preparedStatement.execute();
} catch (SQLException e) {
e.printStackTrace();
}
}
为了方便后期的开发,我们目前字段设计包括
描述 | 列名 | 备注 |
---|---|---|
主键 | _id | |
关键词 | fkey_words | 文本类型 |
描述 | fdescription | 文本类型 |
标签 | ftag | 区分语言或者开发平台的标记 |
存储模式 | fpattern | 用于后续标记代码块的存储模式1代表数据库,2代表文件 |
参数 | fparam | 参数,用于替换代码块中的某些变量,目前只支持单参数 |
代码块 | fdetail | 查询出的代码块 |
作者 | faccount | 目前没啥用,后续可能会有用 |
创建时间 | fdate | 仅做为数据收集 |
接下来就是关键词查询了,这里我没有做过多的匹配查询,我的模糊匹配思路是,将所输入的查询关键词按空格分组,只要有一组左右模糊匹配就可以认定是有效结果,将所有的结果集按照完全是、全匹配、部分匹配的顺序进行排序。
public DefaultListModel<Map<String, String>> queryKey(String key){
String[] words = key.split(" ");
StringBuilder sql = new StringBuilder("SELECT * FROM tb_command WHERE");
StringBuilder orderby = new StringBuilder("ORDER BY (CASE WHEN fkey_words = '");
orderby.append(key).append("' THEN 1 WHEN fkey_words LIKE '%").append(key).append("%' THEN 2 WHEN");
int length = words.length;
for (int i = 0; i < length; i++) {
sql.append(" fkey_words LIKE ? OR");
if (0 == i){
orderby.append(" fkey_words LIKE ").append("'%" + words[i] + "%'");
} else {
orderby.append(" OR fkey_words LIKE ").append("'%" + words[i] + "%'");
}
}
orderby.append(" THEN 3 ELSE 4 END)");
sql.delete(sql.length() - 2, sql.length());
sql.append(orderby.toString());
DefaultListModel<Map<String, String>> data = new DefaultListModel();
Map<String, String> resMap;
try {
PreparedStatement preparedStatement = mConnection.prepareStatement(sql.toString());
for (int i = 1; i <= length; i++) {
preparedStatement.setString(i, "%" + words[i-1] + "%");
}
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()){
resMap = new HashMap<>();
getData(resMap, resultSet);
data.addElement(resMap);
}
} catch (SQLException e) {
e.printStackTrace();
}
return data;
}
这里为了方便结果集的列表展示,我没有采用List的集合,而是使用了ListModel
装载。
剩下就是补充增删改的代码了,至此,我们的1.0版本的数据库模块也完成了。
Swing UI开发
说实话,开发了那么久的Android,Swing的知识忘的都差不多了,而且总是习惯性的用Android的思维去写Swing的界面,折腾了很长时间最后好歹还是有点成绩的。先放上两个文档:
-
JBPopup
顾名思义,弹出菜单,弹出菜单的样式多种多样,是Intelij的自定义控件,弹出菜单样式自定义,通用的样式包括文本弹窗、列表弹窗、组件弹窗等等,这里我们需要一个输入列表弹窗,因此,我们选择
JBPopupFactory.createComponentPopupBuilder
的方式。弹窗除了内容需要设置,也还需要样式的定制,比如尺寸,是否可移动、弹出位置等等。这里为了方便(偷懒),借用了其他插件的源码,通过
UISettings.getInstance().SHOW_MAIN_TOOLBAR ? 135 : 115;
的方式指定在编译器菜单栏下方窗口横向中间的位置弹出,效果和Search Everywhere的弹窗效果类似。 -
Layout组件
上面的弹窗其实看看API手册,翻翻源代码,其实难度并不大,让人颇为为难的还是Layout Manager的问题,也就是布局方式的问题,Swing提供了五种基本布局方式:
BorderLayout 边界布局
CardLayout 卡片布局
GridLayout 网格布局
BoxLayout 盒子布局
FlowLayout 流式布局,默认布局
这些布局的基本情况不在这里赘述,网上的资料也是非常的多。但最后我选择了
MigLayout
-
MigLayout用起来和GridLayout有点类似,说具体点,更像Excel的单元格那样,在一些组合界面来说,对开发者更为友好。使用这种方式必须先安装
JFormDesigner
付费插件,通过工程右键new --> JFormDesigner Form的方式指定JPanel的LayoutManager为MigLayout。按照官网所说,应该还能支持Android平台。这里面讲个概念:约束-Constraints,包括Layout constraints、Column constraints、Row constraints三个约束组成。约束条件包括列换行、合并单元格、指定单元格位置、单元格间距,单元格内对齐方式、组件的尺寸、单元格边界距离等等。
-
JDialog
这里直接右键new一个dialog就可以了,设置带Cancel按钮和Save按钮。
在这里,可以如下图这样配置,就能在对应的Java文件中看到生成的initComponents
方法:
光标所在文本区域的文本修改
这里面要注意的事请只有一个,当获取某些属性的时候,不需要太在意其他内容,但当修改或者插入文本的时候,必须在线程中操作,有些类似于Android的UIThread一样:
WriteCommandAction.runWriteCommandAction(mProject, new Runnable(){
SelectionModel selectionModel = mEditor.getSelectionModel();
CaretModel caretModel = mEditor.getCaretModel();
Document document = mEditor.getDocument();
document.insertString(offset, "insertText");
});
这里要注意的是,如果对光标所选中的文本进行更改比如selectionModel.removeSelection();
等等操作,则会使selectionModel.getSelectedText
为空,所以一定要提前保存变量。
在这里我们可以针对我们之前提的参数替换需求做一个初步的简单处理,对选中文本进行整段代码的替换,如果存在参数,并替换代码的参数。这里用到了正则的处理,\\b" + param + "(?!\")\\b
,用来判断单词的边界,不过也存在一个明显的缺点,就是同样会替换掉String字符串内非""边界部分的单词,后续可以使用PSIFile
来处理这些问题。