AndroidStudio插件开发(进阶篇之Editor)

转载请注明出处:【huachao1001的简书:http://www.jianshu.com/users/0a7e42698e4b/latest_articles】

我们开发AndroidStudio插件,绝大多数插件功能是用在编辑文本上面,让用户开发更便捷。这篇文章主要是介绍Editor部分,看完之后可以开发简单实用的插件啦!在看本文之前,请先确定已经看完《AndroidStudio插件开发(Hello World篇)》《 AndroidStudio插件开发(进阶篇之Action机制)》。因为这两篇是基础,没有这些基础就无法继续往下读。

在本文的最后使用简单的代码实现简单的插件:自动生成Getter和Setter函数的插件。如下图所示,下图中,分别演示了通过点击和使用快捷键的方式触发Action。

自动生成Getter和Setter函数

1. 文本编辑

1.1 CaretModel和SelectionModel

为了能够更灵活地控制Editor,IDEA插件开发中将Editor细分为多个模型。在本文中只简单介绍CaretModel和SelectionModel,除了CaretModel和SelectionModel以外,还有如下几种模型:

  • FoldingModel
  • IndentsModel
  • ScrollingModel
  • ScrollingModel
  • SoftWrapModel

获取Editor的CaretModel和SelectionModel对象方法如下:

@Override
public void actionPerformed(AnActionEvent e) {
    Editor editor = e.getData(PlatformDataKeys.EDITOR); 
    if (editor == null)
        return;
        
    SelectionModel selectionModel = editor.getSelectionModel();
    CaretModel caretModel=editor.getCaretModel();
}

其他模型对象获取方式类似,通过Editor对象的相应函数即可得到。

1.1.1 CaretModel对象

CaretModel对象用于描述插入光标,通过CaretModel对象,可以实现如下功能:

  1. moveToOffset(int offset):将光标移动到指定位置(offset)
  2. getOffset():获取当前光标位置偏移量
  3. getCaretCount:获取光标数量(可能有多个位置有光标)
  4. void addCaretListener(CaretListener listener) ,void removeCaretListener(CaretListener listener):添加或移除光标监听器(CareListener)
  5. Caret addCaret(VisualPosition visualPosition):加入新的光标
  6. ......

1.1.2 SelectionModel对象

SelectionModel对象用于描述光标选中的文本段,通过SelectionModel对象可以实现如下功能:

  1. String getSelectedText() :获取选中部分字符串。
  2. int getSelectionEnd():获取选中文本段末尾偏移量
  3. int getSelectionStart():获取选中文本段起始位置偏移量
  4. void setSelection(int start, int end):设置选中,将staert到end部分设置为选中
  5. void removeSelection():将选中文本段删除
  6. void addSelectionListener(SelectionListener listener):添加监听器,用于监听光标选中变化。
  7. void selectLineAtCaret():将光标所在的行设置为选中。
  8. void selectWordAtCaret(boolean honorCamelWordsSettings):将光标所在的单词设置为选中。honorCamelWordsSettings表示是否驼峰命名分隔,如果为true,则大写字母为单词的边界
  9. ......

1.2 Document对象

与Editor中的其他对象一样,通过Editor对象的一个getter函数即可得到Document对象:

Document document = editor.getDocument();

Document对象用于描述文档文件,通过Document对象可以很方便的对Editor中的文件进行操作。可以做如下这些事情:

  1. String getText()String getText( TextRange range):获取Document对象对应的文件字符串。
  2. int getTextLength():获取文件长度。
  3. int getLineCount():获取文件的行数
  4. int getLineNumber(int offset):获取指定偏移量位置对应的行号offset取值为[0,getTextLength()-1]
  5. int getLineStartOffset(int line):获取指定行的第一个字符在全文中的偏移量,行号的取值范围为:[0,getLineCount()-1]
  6. int getLineEndOffset(int line):获取指定行的最后一个字符在全文中的偏移量,行号的取值范围为:[0,getLineCount()-1]
  7. void insertString(int offset, CharSequence s):在指定偏移位置插入字符串
  8. void deleteString(int startOffset, int endOffset):删除[startOffset,endOffset]位置的字符串,如果文件为只读,则会抛异常。
  9. void replaceString(int startOffset, int endOffset, CharSequence s):替换[startOffset,endOffset]位置的字符串为s
  10. void addDocumentListener( DocumentListener listener):添加Document监听器,在Document内容发生变化之前和变化之后都会回调相应函数。
  11. ......

1.3 实现自动生成Getter和Setter函数的插件

有了上面的认识后,我们可以开始写个简单的Getter和Setter函数插件了。首先创建一个Action,名为GetterAndSetter,并在plugin.xml中注册。plugin.xml的<acitons>标签部分如下:

<actions> 
    <action id="StudyEditor.GetterAndSetter" class="com.huachao.plugin.GetterAndSetter" text="Getter And Setter"
            description="生成Getter和Setter方法">
          <add-to-group group-id="EditorPopupMenu" anchor="first"/>
          <keyboard-shortcut keymap="$default" first-keystroke="ctrl alt G"/>
    </action>
</actions>

通过前面两篇文章的学习,我们知道,定义Action时需要重写actionPerformed和update函数。

@Override
public void actionPerformed(AnActionEvent e) {
    //获取Editor和Project对象
    Editor editor = e.getData(PlatformDataKeys.EDITOR);
    Project project = e.getData(PlatformDataKeys.PROJECT);
    if (editor == null||project==null)
        return;
    
    //获取SelectionModel和Document对象
    SelectionModel selectionModel = editor.getSelectionModel();
    Document document = editor.getDocument();
    
    //拿到选中部分字符串
    String selectedText = selectionModel.getSelectedText();
    
    //得到选中字符串的起始和结束位置
    int startOffset = selectionModel.getSelectionStart();
    int endOffset = selectionModel.getSelectionEnd(); 
    
    //得到最大插入字符串(即生成的Getter和Setter函数字符串)位置
    int maxOffset = document.getTextLength() - 1;
    
    //计算选中字符串所在的行号,并通过行号得到下一行的第一个字符的起始偏移量
    int curLineNumber = document.getLineNumber(endOffset);
    int nextLineStartOffset = document.getLineStartOffset(curLineNumber + 1);

    //计算字符串的插入位置
    int insertOffset = maxOffset > nextLineStartOffset ? nextLineStartOffset : maxOffset;
    
    //得到选中字符串在Java类中对应的字段的类型
    String type = getSelectedType(document, startOffset);

    //对文档进行操作部分代码,需要放入Runnable接口中实现,由IDEA在内部将其通过一个新线程执行
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            //genGetterAndSetter为生成getter和setter函数部分
            document.insertString(insertOffset, genGetterAndSetter(selectedText, type));
        }
    };
    
    //加入任务,由IDEA调度执行这个任务
    WriteCommandAction.runWriteCommandAction(project, runnable);

}

@Override
public void update(AnActionEvent e) {
    Editor editor = e.getData(PlatformDataKeys.EDITOR);
    SelectionModel selectionModel = editor.getSelectionModel();
    
    //如果没有字符串被选中,那么无需显示该Action
    e.getPresentation().setVisible(editor != null && selectionModel.hasSelection());
}

剩下的还有获取选中字段的类型和生成Getter、Setter函数两个部分,两个函数如下:

private String getSelectedType(Document document, int startOffset) {

    String text = document.getText().substring(0, startOffset).trim();
    int startIndex = text.lastIndexOf(' ');

    return text.substring(startIndex + 1);
}

private String genGetterAndSetter(String field, String type) {
  if (field == null || (field = field.trim()).equals(""))
      return "";
  String upperField = field;
  char first = field.charAt(0);
  if (first <= 'z' && first >= 'a') {
      upperField = String.valueOf(first).toUpperCase() + field.substring(1);
  }
  String getter = "\tpublic TYPE getUpperField(){ \n\t\treturn this.FIELD;\n\t}";
  String setter = "\tpublic void setUpperField(TYPE FIELD){\n\t\tthis.FIELD=FIELD;\n\t}";

  String myGetter = getter.replaceAll("TYPE", type).replaceAll("UpperField", upperField).replaceAll("FIELD", field);
  String mySetter = setter.replaceAll("TYPE", type).replaceAll("UpperField", upperField).replaceAll("FIELD", field);

  return "\n"+myGetter + "\n" + mySetter + "\n";
}

运行后如下:

自动生成Getter和Setter函数

注意:在对Document进行修改时,需要实现Runnable接口并将修改部分代码写入run函数中,最后通过 WriteCommandAction的runWriteCommandAction函数执行。

2. Editor的坐标系统:位置和偏移量

前面小节我们知道,通过CaretModel对象我们可以获取当前光标位置。但在Editor中位置分为两种,一种是逻辑位置,对应LogicalPosition类;另一种是视觉位置,对应VisualPosition类。

LogicalPosition与VisualPosition的区别通过如下图很显然能区分开来。

LogicalPosition与VisualPosition

上如中,光标的坐标为:

LogicalPosition:(13,6)
VisualPosition:(9,6)

注意,行号和列号都是从0开始。

另外,获取LogicalPosition和VisualPosition方法如下:

@Override
public void actionPerformed(AnActionEvent e) {
    //获取Editor和Project对象
    Editor editor = e.getData(PlatformDataKeys.EDITOR);
    Project project = e.getData(PlatformDataKeys.PROJECT);
    if (editor == null || project == null)
        return;
    CaretModel caretModel = editor.getCaretModel();
    LogicalPosition logicalPosition = caretModel.getLogicalPosition();
    VisualPosition visualPosition = caretModel.getVisualPosition();

    System.out.println(logicalPosition + "," + visualPosition);
}

3. Editor中的按键事件

为了监听按键时间,专门提供了TypedActionHandler类,我们只需继承TypedActionHandler,并重写execute函数即可。注意,只能监听可打印字符对应的按键。

package com.huachao.plugin;

import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.actionSystem.TypedActionHandler;
import org.jetbrains.annotations.NotNull;

/**
 * Created by huachao on 2016/12/26.
 */
public class MyTypedActionHandler implements TypedActionHandler {
    @Override
    public void execute(@NotNull Editor editor, char c, @NotNull DataContext dataContext) {
        System.out.println(c);
    }
}

TypedAction专门处理按键相关操作,定义了TypedActionHandler后,接下来就是将自定义的TypedActionHandler加入到TypedAction中。如何获取TypedAction对象呢?具体如下:

final EditorActionManager actionManager = EditorActionManager.getInstance();
final TypedAction typedAction = actionManager.getTypedAction();
typedAction.setupHandler(new MyTypedActionHandler());

上述代码即可将自定义的按键处理器成功加入,现在有个问题是,上面这段代码应该放入到哪里呢?之前我们都是重写AnAction的actionPerformed和update函数就行,能不能将上面这段代码放入到actionPerformed中呢?显然这是可以的,但是这样的话就得先点击当前Action后才能使MyTypedActionHandler被加入,并且每点击一次,就会创建新的MyTypedActionHandler并将原先的替换。我们可以把上面这段代码加入到Action的构造函数中,或者是在Action中创建static块。

注意:只能设置一个监听,如果自定义了按键监听,而不做其他处理的话,会使得原先IDEA中的按键监听无法处理,导致无法正常在输入框中输入。

为了能更充分理解TypedActionHandler,我们实现一个简单功能的插件:在输入字符的同时,在文档的开头也插入同样的字符。

首先定义TypedActionHandler:

package com.huachao.plugin;

import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.editor.CaretModel;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.editor.actionSystem.TypedActionHandler;
import org.jetbrains.annotations.NotNull;

/**
 * Created by huachao on 2016/12/26.
 */
public class MyTypedActionHandler implements TypedActionHandler {
    private TypedActionHandler oldHandler;
    private boolean isBegin = true;
    private int caretLine = 0;

    @Override
    public void execute(@NotNull Editor editor, char c, @NotNull DataContext dataContext) {
        if (oldHandler != null)
            oldHandler.execute(editor, c, dataContext);

        Document document = editor.getDocument();
        CaretModel caretModel = editor.getCaretModel();
        int caretOffset = caretModel.getOffset();
        int line = document.getLineNumber(caretOffset);
        if (isBegin) {
            document.insertString(document.getLineStartOffset(line), String.valueOf(c) + "\n");
            caretLine = line + 1;
            isBegin = false;
        } else {
            if (line != caretLine) {
                isBegin = true;
                execute(editor, c, dataContext);
            } else {
                document.insertString(document.getLineEndOffset(line - 1), String.valueOf(c));
            }
        }
        System.out.println(caretLine + "," + line);

    }

    public void setOldHandler(TypedActionHandler oldHandler) {
        this.oldHandler = oldHandler;
    }
}



将我们定义的TypedActionHandler设置进去,只需实现一个简单Action。
```java
package com.huachao.plugin;

import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.editor.actionSystem.EditorActionManager;
import com.intellij.openapi.editor.actionSystem.TypedAction;
import com.intellij.openapi.editor.actionSystem.TypedActionHandler;

/**
 * Created by huachao on 2016/12/26.
 */
public class InsertCharAction extends AnAction {
    public InsertCharAction() {
        final EditorActionManager actionManager = EditorActionManager.getInstance();
        final TypedAction typedAction = actionManager.getTypedAction();
        MyTypedActionHandler handler = new MyTypedActionHandler();
        //将自定义的TypedActionHandler设置进去后,
        //返回旧的TypedActionHandler,即IDEA自身的TypedActionHandler
        TypedActionHandler oldHandler = typedAction.setupHandler(handler);
        handler.setOldHandler(oldHandler);
    }

    @Override
    public void actionPerformed(AnActionEvent e) {
        
    }
}

运行结果如下:


重复输入

参考资料

Document类源码:点击这里
官方文档:http://www.jetbrains.org/intellij/sdk/docs/tutorials/editor_basics.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容