还在写重复代码?写个超简单插件来自动生成吧!

携手创作,共同成长!这是我参与「掘金日新计划 · 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,还可以分开单独设置isEnabledisVisible

获取文本文件的内容

想要在文件中生成代码,肯定要先获得这个文件的对象,在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. 虽然肉眼所见代码里的行数都是从1开始的,但根据偏移量获取的行数其实都是index,从0开始的,写的时候不要搞混了。
  2. 增删改文本,需要给文本加锁,然后才能安全的操作。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
}
复制代码

最后在ActionactionPerformed方法中调用,

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())
}
复制代码

即可完成。点击运行,效果如下: image.png

image.png

可以看到右键点击弹出的EditPopupWindow里显示了插件操作,但是执行后,并没有插入在光标所在的位置,而是插入在了第一行,这是因为我们的偏移量设置了0导致的。

如何找到正确的插入位置(偏移量),那就要用到EditorModels模型

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")
}

复制代码

image.png

有了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)
    }
}

复制代码

效果如下:

动画.gif

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。逻辑位置,就是实际的位置,虚拟位置,则是折叠后的肉眼看到的位置。

下面用一个示例图来说明究竟什么是逻辑位置,什么是虚拟位置。

image.png

图中代码被折叠,光标所在的位置,实际是第9行,offset为1,逻辑位置是(8,1),而虚拟位置则是肉眼可见的第7行,最终坐标(6,22),由此可见两者的区别。

猜你喜欢

转载自juejin.im/post/7130278049186054175