携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情
本文又名:Android Studio插件开发(二):Editors编辑篇
之前的阅读量太低了,换个名字看看会不会高点,o(╥﹏╥)o
概述
作为连Setter和Getter都要生成的程序猿来说,时不时还要手写重复的代码简直不能忍。而本篇文章,讲的就是如何开发一个简单的自动生成插件,来帮你完成一键生成重复代码。
最终要实现的效果有:
- 智能识别 只有在需要满足条件且需要生成的地方,才会提供生成功能
- 自动生成 选择文本,根据关键词,生成特定的代码
创建插件
上一篇文章有讲过,首先要创建一个AutoEditorAction
,然后在plugin.xml
中注册
class AutoEditorAction: AnAction() {
override fun actionPerformed(event: AnActionEvent) {
}
}
复制代码
<action class="com.lzy.plugindemo.AutoEditorAction"
id="AutoEditorAction"
text="Auto Input"
description="Auto input text">
//添加在编辑弹框的第一位
<add-to-group group-id="EditorPopupMenu" anchor="first"/>
</action>
复制代码
定义菜单操作的可见性
应该有注意过,Android Studio里菜单栏下面的功能,并非一直可以点击,有时候是置灰状态,有时候甚至没有显示出来。这些都是通过AnAction
里的update
方法来控制的。
在AutoEditorAction
中重写update
方法
override fun update(event: AnActionEvent) {
super.update(event)
}
复制代码
我们希望只有编辑区有打开代码的时候,才可以使用。那么进行如下改动:
override fun update(event: AnActionEvent) {
//获取工程对象
val project: Project? = event.project
//获取编辑对象
val editor: Editor? = event.getData(CommonDataKeys.EDITOR)
//设置不可用且不可见
event.presentation.isEnabledAndVisible = project != null && editor != null
}
复制代码
除了上面用到的isEnabledAndVisible
,还可以分开单独设置isEnabled
或isVisible
获取文本文件的内容
想要在文件中生成代码,肯定要先获得这个文件的对象,在Intellij里面,使用Document来表示加载到内存中并在基于 IntelliJ 平台的 IDE 编辑器中打开的文本文件的内容。
Document
可以通过Editor
获取
editor.getDocument()
复制代码
下面是几个常用的方法:
//获取全文
String getText()
//指定位置插入/删除/替换
void insertString(int offset, @NonNls @NotNull CharSequence s)
void deleteString(int startOffset, int endOffset)
void replaceString(int startOffset, int endOffset, @NlsSafe @NotNull CharSequence s)
//获取全文长度
int getTextLength()
//获取指定偏移量对应的行数
int getLineNumber(int offset)
//获取文件的总行数
int getLineCount();
//获取某一行,第一个字符的偏移量
int getLineStartOffset(int line)
复制代码
值得注意的有两点:
- 虽然肉眼所见代码里的行数都是从1开始的,但根据偏移量获取的行数其实都是index,从0开始的,写的时候不要搞混了。
- 增删改文本,需要给文本加锁,然后才能安全的操作。Intellij提供了
WriteCommandAction
来实现安全操作:
WriteCommandAction.runWriteCommandAction(project, () ->
document.replaceString(start, end, "replace me")
);
复制代码
了解了这些方法,我们就可以操作这个文件了。 先定义一个插入方法:
fun inputText(project: Project, document: Document, offset: Int, text: String){
WriteCommandAction.runWriteCommandAction(project) {
document.insertString(offset, text)
}
}
复制代码
再提供要插入的内容(这里随便):
fun getText(): String{
// \t相当于一个tab,\n换行
val text = "\tinner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {\n\n\t}"
return text
}
复制代码
最后在Action
的actionPerformed
方法中调用,
override fun actionPerformed(event: AnActionEvent) {
val project: Project? = event.project
val editor: Editor? = event.getData(CommonDataKeys.EDITOR)
if (project == null || editor == null){
return
}
val document = editor.document
inputText(project, document, 0, getText())
}
复制代码
即可完成。点击运行,效果如下:
可以看到右键点击弹出的EditPopupWindow里显示了插件操作,但是执行后,并没有插入在光标所在的位置,而是插入在了第一行,这是因为我们的偏移量设置了0导致的。
如何找到正确的插入位置(偏移量),那就要用到Editor
的Models
模型
Models模型
Editor提供了很多模型,包括:
- SelectionModel
- CaretModel
- FoldingModel
- IndentsModel
- ScrollingModel
- SoftWrapModel
在这里,先讲一下SelectionModel
SelectionModel
SelectionModel,按照官方的说法是,为文本编辑器选中文本以及检索选中内容的信息提供服务(原谅我渣翻译)。
Provides services for selecting text in the IDE's text editor and retrieving information about the selection.
我们可以通过它来判断是否有选中文本,获取选中的文本内容,以及找到相应的位置偏移量等。
那我们来写一个简单的方法测试一下:
val selectionModel = editor.selectionModel
if (selectionModel.hasSelection()) {
val text = selectionModel.selectedText
val startOffset = selectionModel.selectionStart
val endOffset = selectionModel.selectionEnd
println("text: $text")
println("startOffset: $startOffset")
println("endOffset: $endOffset")
}
复制代码
有了SelectionModel
,结合Document
,我们就可以在指定位置,生成动态的代码了。下面我们来生成RecyclerView.Adapter
的部分代码
//初始状态
class TabAdapter {
}
//目标
class TabAdapter(val mContext: Context) : RecyclerView.Adapter<TabAdapter.ViewHolder>() {
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
}
}
复制代码
实现:
if (selectionModel.hasSelection()) {
val text = selectionModel.selectedText
val startOffset = selectionModel.selectionStart
val endOffset = selectionModel.selectionEnd
if (text.isNullOrEmpty()) {
//判断选中的文本是否为空
return
}
//获取选中文本所在行
val curLineNumberIndex = document.getLineNumber(startOffset)
//获取当前文本总行数
val lineCount = document.lineCount
//要插入的代码,kotlin使用$替换关键字
val insertText = "(val mContext: Context) : RecyclerView.Adapter<$text.ViewHolder>()"
//安全写入,根据需求就插入在选中文本的尾部
WriteCommandAction.runWriteCommandAction(project) {
document.insertString(endOffset, insertText)
}
// \t tab, \n 换行
val insertText2 = "\n\tinner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {\n\n\t}\n"
//插入的位置计算
val insertOffset = if (curLineNumberIndex < lineCount - 1) {
//当前行小于总行数,插入位置为 下一行的开头
document.getLineStartOffset(curLineNumberIndex + 1)
} else {
//否则插入在最后一个大括号前
document.text.lastIndexOf("}")
}
WriteCommandAction.runWriteCommandAction(project) {
document.insertString(insertOffset, insertText2)
}
}
复制代码
效果如下:
SelectionModel中不止有上面用到的方法,基本上所有跟选择有关的都有提供,具体请查阅源码,这里就不在赘述。
好啦,到这里,我们就已经掌握了在指定位置自动插入代码的方法啦,可以自己实现一个自动生成代码的插件啦
CaretModel
(补充一下Editor里的其他知识,可跳过)
CaretModel,按照官方的说法是,为移动插入光标以及检索插入光标位置提供服务
Provides services for moving the caret and retrieving information about caret position.
我们可以通过它,来操作插入的光标。常用的方法有:
//移动光标到逻辑位置
default void moveToLogicalPosition(@NotNull LogicalPosition pos)
//移动光标到虚拟位置
default void moveToVisualPosition(@NotNull VisualPosition pos)
//移动光标到偏移位置
default void moveToOffset(int offset)
//获取逻辑位置
default LogicalPosition getLogicalPosition()
//获取虚拟位置
default VisualPosition getVisualPosition()
//监听光标移动、光标增加、光标删除
void addCaretListener(@NotNull CaretListener var1)
//给指定的虚拟位置插入一个新的光标
default Caret addCaret(@NotNull VisualPosition pos)
复制代码
上面的方法很多都区分逻辑位置LogicalPosition
和虚拟位置VisualPosition
。逻辑位置,就是实际的位置,虚拟位置,则是折叠后的肉眼看到的位置。
下面用一个示例图来说明究竟什么是逻辑位置,什么是虚拟位置。
图中代码被折叠,光标所在的位置,实际是第9行,offset为1,逻辑位置是(8,1),而虚拟位置则是肉眼可见的第7行,最终坐标(6,22),由此可见两者的区别。