Idea插件开发入门

内容范围

本文档旨在介绍IntelliJ Platform Plugin SDK中的一些概念和基础知识,和如何开发idea插件。

Idea plugin可以用来干什么

大多数的插件分为以下几种:

如何创建你的Plugin项目

创建Plugin项目的方式有三种:

概念及示例

插件结构

插件安装后的结构

  1. 插件相关目录:
    • Idea安装后自带的插件位置:/Applications/IntelliJ IDEA.app/Contents/plugins
    • 自己安装的插件位置:/Users/dz0400284/Library/Application Support/JetBrains/IntelliJIdea2020.3/plugins
  2. 插件安装后目录结构:
  • 2.1 本身插件没有其他第三方jar包依赖的,构建插件时,会被打成Jar包。安装后也是直接将Jar包放在插件安装目录
.IntelliJIDEAx0/
└── plugins
    └── sample.jar
        ├── com/foo/...
        │   ...
        │   ...
        └── META-INF
            ├── plugin.xml
            ├── pluginIcon.svg
            └── pluginIcon_dark.svg
  • 2.2 本身插件其他第三方jar包依赖的,构建插件时,会被打成Zip包。安装后,会将Zip包直接解压到插件安装目录,结构如下图:
.IntelliJIDEAx0/
└── plugins
    └── sample
        └── lib
            ├── lib_foo.jar
            ├── lib_bar.jar
            │   ...
            │   ...
            └── sample.jar
                ├── com/foo/...
                │   ...
                │   ...
                └── META-INF
                    ├── plugin.xml
                    ├── pluginIcon.svg
                    └── pluginIcon_dark.svg

插件类加载器

每一个运行的插件都有单独的类加载器。默认情况下,本插件的类加载器是加载不到的class会交由Idea的类加载器去加载。在依赖其他插件的class时需要在本插件的plugin.xml中指定<depends>依赖的插件,这样就能在本插件的来加载器加载不到类时,Idea会使用依赖插件的类加载器去加载到对应的类。

基于之上的类加载器层级结构,在插件代码使用SPI相关功能时,代码的外围将当前线程的线程上下文类加载器(TCCL)设置为该插件的类加载器,运行结束后将原先的值恢复。
例如:

public class PluginUtils{
    public static <T> T invokeInServiceLoader(Callable<T> callable){
    ClassLoader current = Thread.currentThread().getContextClassLoader();
    try{
    Thread.currentThread().setContextClassLoader(PluginUtils.class.getClassLoader());
    return callable.call();
    }catch (Exception e) {
    throw new RuntimeException(e);
    }finally {
        Thread.currentThread().setContextClassLoader(current);
    }
  }
}
  • 在插件开发过程中碰到java.lang.LinkageError,这个是因为引用对象的Class和实际对象的Class是不同classloader加载的。一般出现该问题的原因是:插件工程中依赖了系统工程中依赖了的jar(解决:exclude掉相关依赖),或是插件工程创建了FQN与系统工程中依赖jar相同的类(解决:一般是误创建了,删除就行了).可以查看/Applications/IntelliJ IDEA.app/Contents/lib中查看Idea依赖的一些jar包。

Action

action是开发插件功能最常用的方式,自定义Antion类继承AnAction类,实现actionPerformed方法。当antion对应的菜单栏选项或工具栏按钮被选中时,就会执行对应Action的actionPerformed方法。antion会以group的形式放在一起,group中也能包含其他group。之后会详细介绍如何创建Action。

Extension

插件扩展是扩展idea platform功能的常用方式,比如:com.intellij.toolWindow扩展点支持插件新增工具窗口到idea;com.intellij.applicationConfigurable和com.intellij.projectConfigurable扩展点支持插件新增页面到Setting和Preferences窗口。你可以从 这里 中找到ideaplatform及相关捆绑插件的所有扩展点。

Service

类似Spring中的@Service和@Component,service会在其对应的level中保持单例,而且是在第一次获取时创建实例。如果Services需要在关闭的时候,增加一些处理逻辑时,可以实现Disposable接口并实现dispose()方法。

  • 类型:Service分为三种:application level services, Project level Services和Model level Services。分别在各自的范围内保持单例。Note:不推荐使用Model level Service,因为会随着Model的增多而增加内存的使用。
  • 构造方法:Project level Services和Model level Services的构造方法可以增加一个Project/Model参数。tips:避免将耗时的初始化逻辑放在构造方法内。
  • Light Services: 如果Services不允许被继承,可以不在plugin.xml中注册,直接使用@Service注解标注,也是在第一次调用是初始化。但需要有一些限制条件:
    1. class必须是final的
    2. 不支持构造器注入方式。
    3. 如果Service实现了PersistentStateComponent,@State(storages={@Storage(value=“xxxx.xml”, roamingType=RoamingType.DISABLED)})中的roamingType=RoamingType.DISABLED需要设置。
  • Service示例:
    1. plugin.xml中<extendsions>下配置扩展点:com.intellij.applicationService或com.intellij.projectService
<idea-plugin>
  <extensions defaultExtensionNs="com.intellij">
    <!-- Declare the application level service -->
    <applicationService serviceInterface="mypackage.MyApplicationService"
                        serviceImplementation="mypackage.MyApplicationServiceImpl" />
                        
    <!-- Declare the project level service -->
    <projectService serviceInterface="mypackage.MyProjectService"
                    serviceImplementation="mypackage.MyProjectServiceImpl" />
  </extensions>
</idea-plugin>
  1. service类实现:
    定义Service时,可以定义接口,也可以不定义接口。不定义接口时,serviceInterface和serviceImplementation配置成一样就行。
  2. 如何获取Service:
// application level service
MyApplicationService applicationService = ApplicationManager.getApplication().getService(MyApplicationService.class);
// project level service
MyProjectService projectService = project.getService(MyProjectService.class)

Listener

listener允许插件订阅通过Message Bus分发的事件。
Listener类型也分为两种:application level listener和project level listener。
在plugin.xml中声明式定义listener会优于在代码中注册listener,优势在于声明式只会在对应事件第一次触达的时候才会构造实例。

  • application level listener:
    plugin.xml中配置
<idea-plugin>
    <applicationListeners>
      <listener class="myPlugin.MyListenerClass" topic="BaseListenerInterface"/>
    </applicationListeners>
</idea-plugin>
- topic:listener感兴趣的event。通常是event对应topic实例的listenerClass。
- class : 接受event的实现类。
  • 代码注册listener和声明式listener的两种方式,下面以监听Virtual File事件为例:Topic对应的是:VirtualFileManager.VFS_CHANGES,对应的listenerClass是:com.intellij.openapi.vfs.newvfs. BulkFileListener
  • 代码注册listener:
messageBus.connect().subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener() {
    @Override
    public void after(@NotNull List<? extends VFileEvent> events) {
        // handle the events
    }
});
  • 声明式:
    plugin.xml中配置:
<idea-plugin>
  <applicationListeners>
      <listener class="myPlugin.MyVfsListener"
            topic="com.intellij.openapi.vfs.newvfs.BulkFileListener"/>
    </applicationListeners>
</idea-plugin>

listener实现类:

public class MyVfsListener implements BulkFileListener {
    @Override
    public void after(@NotNull List<? extends VFileEvent> events) {
        // handle the events
    }
}
  • project level listener: 允许listener构造方法有Project参数。
    pugin.xml中配置:
<idea-plugin>
    <projectListeners>
        <listener class="MyToolwindowListener"
                  topic="com.intellij.openapi.wm.ex.ToolWindowManagerListener" />
    </projectListeners>
</idea-plugin>

listener实现类:

public class MyToolwindowListener implements ToolWindowManagerListener {
    private final Project project;

    public MyToolwindowListener(Project project) {
        this.project = project;
    }

    @Override
    public void stateChanged() {
        // handle the state change
    }
}

Extension Point

通过定义插件扩展点,你可以允许别的插件来扩展你的插件功能。
扩展点分为两种类型:

  • 接口扩展点(Interface Extension point):你定义接口扩展点,别的插件提供实现类。
  • Bean扩展点(Bean Extension Point): 你定义需要的data,别人插件扩展的时候,直接注入data。

Bean扩展点示例:

  1. 定义扩展点:
    你的插件plugin.xml中:
<idea-plugin>
  <id>my.plugin</id>
  <extensionPoints>
    <extensionPoint name="myExtensionPoint1"
                    beanClass="com.myplugin.MyBeanClass"/>
    <extensionPoint name="myExtensionPoint2"
                    interface="com.myplugin.MyInterface"/>
  </extensionPoints>
</idea-plugin>
  1. 其中的MyBeanClass:
public class MyBeanClass extends AbstractExtensionPointBean {

  @Attribute("key")
  public String key;

  @Attribute("implementationClass")
  public String implementationClass;

  public String getKey() {
    return key;
  }

  public String getClass() {
    return implementationClass;
  }
}
  1. 其他插件扩展:
<idea-plugin>
  <id>another.plugin</id>

  <!-- declare dependency on plugin defining extension point -->
  <depends>my.plugin</depends>

  <!-- use "my.plugin" namespace -->
  <extensions defaultExtensionNs="my.plugin">
    <myExtensionPoint1 key="someKey"
                       implementationClass="another.some.implementation.class"/>
    <myExtensionPoint2 implementation="another.MyInterfaceImpl"/>
  </extension>
</idea-plugin>
  1. 我的插件中使用别人的扩展实现:
public class MyExtensionUsingService {

    private static final ExtensionPointName<MyBeanClass> EP_NAME =
      ExtensionPointName.create("my.plugin.myExtensionPoint1");

    public void useExtensions() {
      for (MyBeanClass extension : EP_NAME.getExtensionList()) {
        String key = extension.getKey();
        String clazz = extension.getClass();
        // ...
      }
    }
}

plugin.xml配置文件

<idea-plugin url="https://www.jetbrains.com/idea">
  <!-- 插件名字,安装完会在Idea的Plugin界面显示-->
  <name>Vss Integration</name>
  <!-- 你的插件ID,要确保唯一 -->
  <id>com.jetbrains.vssintegration</id>
  <!-- 插件功能描述 -->
  <description>Integrates Volume Snapshot Service W10</description>
  <!-- 版本修改内容 -->
  <change-notes>Initial release of the plugin.</change-notes>
  <!-- 插件版本  -->
  <version>1.0.0</version>
  <!-- 开发者信息 -->
  <vendor url="https://www.company.com" email="support@company.com">A Company Inc.</vendor>
  <!-- 必选插件或Module 依赖,如果idea中没有提供或安装,本插件将无法启用 -->
  <depends>com.intellij.modules.platform</depends>
  <depends>com.third.party.plugin</depends>
  <!-- 可选插件依赖,不会影响本插件的使用,但是会影响该插件的部分功能 -->
  <depends optional="true" config-file="mysecondplugin.xml">com.MySecondPlugin</depends>
  <!-- 插件兼容版本  -->
  <idea-version since-build="193" until-build="193.*"/>
  <!-- 资源定位,可以把一些长文本的描述放在配置文件中,对应文件定位:/messages/MyPluginBundle.properties -->
  <resource-bundle>messages.MyPluginBundle</resource-bundle>
  <!-- application应用组件,已经不推荐使用,不做介绍 -->
  <application-components>
    <component>
      <!-- Component's interface class -->
      <interface-class>com.foo.Component1Interface</interface-class>
      <!-- Component's implementation class -->
      <implementation-class>com.foo.impl.Component1Impl</implementation-class>
    </component>
  </application-components>
  <!-- 组件,已经不推荐使用,不做介绍 -->
  <project-components>
    <component>
      <!-- Interface and implementation classes are the same -->
      <implementation-class>com.foo.Component2</implementation-class>
      <!-- If the "workspace" option is set "true", the component
           saves its state to the .iws file instead of the .ipr file.
           Note that the <option> element is used only if the component
           implements the JDOMExternalizable interface. Otherwise, the
           use of the <option> element takes no effect.  -->
      <option name="workspace" value="true" />
      <!-- If the "loadForDefaultProject" tag is present, the project component is instantiated also for the default project. -->
      <loadForDefaultProject/>
    </component>
  </project-components>

  <!-- Module组件,不推荐使用 -->
  <module-components>
    <component>
      <implementation-class>com.foo.Component3</implementation-class>
    </component>
  </module-components>

  <!-- Actions -->
  <actions>
    <action id="VssIntegration.GarbageCollection" class="com.foo.impl.CollectGarbage" text="Collect _Garbage" description="Run garbage collector">
      <!-- 设置快捷键 -->
      <keyboard-shortcut first-keystroke="control alt G" second-keystroke="C" keymap="$default"/>
    </action>
  </actions>
  <!-- 扩展点定义 -->
  <extensionPoints>
    <extensionPoint name="testExtensionPoint" beanClass="com.foo.impl.MyExtensionBean"/>
  </extensionPoints>
  <!-- 扩展点实现 -->
  <extensions defaultExtensionNs="VssIntegration">
    <testExtensionPoint implementation="com.foo.impl.MyExtensionImpl"/>
  </extensions>
  <!-- Application-level listeners -->
  <applicationListeners>
    <listener class="com.foo.impl.MyListener" topic="com.intellij.openapi.vfs.newvfs.BulkFileListener"/>
  </applicationListeners>
  <!-- Project-level listeners -->
  <projectListeners>
    <listener class="com.foo.impl.MyToolwindowListener" topic="com.intellij.openapi.wm.ex.ToolWindowManagerListener"/>
  </projectListeners>
</idea-plugin>

Logo配置

简单讲解,具体规则参考:官网Logo说明

插件依赖

添加强插件依赖:依赖插件未安装或未启用时,本插件也无法使用。
plugin.xml

<depends>org.another.plugin</depends>

添加可选依赖插件:依赖插件未安装或未启用时,本插件部分功能不可用。
plugin.xml

<depends optional="true" config-file="myPluginId-optionalPluginName.xml">dependency.plugin.id</depends>

myPluginId-optionalPluginName.xml中定义你依赖该插件的相关功能点。

<idea-plugin>
   <extensions defaultExtensionNs="com.intellij">
      <annotator language="kotlin" implementationClass="com.example.MyKotlinAnnotator"/>
</extensions>

Disposer

idea使用Disposer来管理资源的清除。Disposer最常管理的是Listener,除此之外还可能是:文件操作和数据库连接,缓存及其他数据结构。需要释放资源的实例可是实现Disposable接口,实现dispose()方法去释放资源。

  • 自动处理对象:有一些实现了Disposable接口的对象会被自动执行dispose()方法。比如说:Service,Application-level services会在idea关闭或提供该service的插件卸载时;Project-level Service在对应project关闭或提供该service的插件卸载时。

线程和消息架构

线程:

详细说明参考:官网文章

  1. 读写锁:通常idea中与代码相关的数据结构由单个的读写锁来控制读写。比如:PSI(Program Structure Interface),VFS(Virtual File System), Project Root Model.
    • 读取数据:允许从任何线程读取数据。从UI线程读取数据不需要任何特殊的工作。然而,从任何其他线程执行的读操作需要通过使用ApplicationManager.getApplication(). runReadAction()或更简短的ReadAction run()/ compute()来包装在读操作中。相应的对象不能保证在几个连续的读取操作之间存活。作为一个经验法则,每当开始一个读取操作时,检查PSI/VFS/project/module是否仍然有效。
    • 写入数据:只允许从UI线程写入数据,并且写入操作总是需要用ApplicationManager.getApplication(). runwriteaction()或更简短的WriteAction run()/ compute()包装在一个写入操作中。只允许在写安全的上下文中修改模型,包括用户操作和来自它们的invokeLater()调用(参见下一节)。你不能从UI渲染器或SwingUtilities.invokeLater()调用中修改PSI、VFS或项目模型。
  2. 后台执行:Progressmanager.getInstance().run()和其他方法。

总结:
读写代码相关的数据结构时,建议使用读写锁覆盖,读操作:使用ApplicationManager.getApplication(). runReadAction()或ReadAction.run()/compute()。写操作:ApplicationManager.getApplication(). runWriteAction()或WriteAction.run()/compute()。读取数据之前判断读取的PSI,VFS或project Model是否还有效。

消息传递机制:

详细阅读,参考:官网文章

核心概念:

  • Topic:client可以订阅特定某个message bus上的特定topic,也可以发送消息到执行message bus上的特定topic。
  • Connection: 管理特定客户端对特定Message Bus的所有订阅。
  • Message Bus:

特性:传播机制,内嵌消息。阅读:官网文章

使用说明:

  1. 定义接口和topic:
public interface ChangeActionNotifier {

    Topic<ChangeActionNotifier> CHANGE_ACTION_TOPIC = Topic.create("custom name", ChangeActionNotifier.class)

    void beforeAction(Context context);
    void afterAction(Context context);
}
  1. 发送消息
public void doChange(Context context) {
    ChangeActionNotifier publisher = myBus.syncPublisher(ActionTopics.CHANGE_ACTION_TOPIC);
    publisher.beforeAction(context);
    try {
        // Do action
        // ...
    } finally {
        publisher.afterAction(context)
    }
}
  1. 订阅消息:
    方式有两种:
  • 在代码中订阅:
public void init(MessageBus bus) {
    bus.connect().subscribe(ActionTopics.CHANGE_ACTION_TOPIC, new ChangeActionNotifier() {
        @Override
        public void beforeAction(Context context) {
            // Process 'before action' event.
        }
        @Override
        public void afterAction(Context context) {
            // Process 'after action' event.
        }
    });
}
  • 在plugin.xml中配置<applicationListeners>或<projectListeners>
<idea-plugin>
    <applicationListeners>
      <listener class="myPlugin.MyChangeActionNotifierClass” topic="ChangeActionNotifier"/>
    </applicationListeners>
</idea-plugin>

Log and Runtime Info

Log:

默认:INFO及以上级别的log会输出到idea.log,对应路径:插件项目目录/build/idea-sandbox/system/log/idea.log

import com.intellij.openapi.diagnostic.Logger;

public class MyPluginFunctionality {

  private static final Logger LOG =
          Logger.getInstance(MyPluginFunctionality.class);

  public void someMethod() {
    LOG.info("someMethod() was called");
  }
}

Runtime Info

  • ApplicationInfo:可以获取Idea的版本信息及plugin.xml中vendor信息
  • SystemInfo: 获取操作系统及JVM相关信息
  • PathManager:Idea的相关路径信息

用户交互组件

不做扩展,感兴趣的可以阅读:官网文章

持久化模型

使用场景:
当你需要保存相关数据到application,project,model级别时。
基础知识:

  • 继承PersistentStateComponent接口,实现T getState()和loadState(T t)。会序列化其中public的属性,@Tag, @Attribute, @Property, @MapAnnotation, @AbstractCollection注解的private属性及bean properties(看着是),以xml形式存储。
  • 执行时机:
    • loadState(T t):会在对应组件被创建时执行。及存储数据的文件被外部修改后。
    • getState(): 需要保存数据的时候被调用。如果返回的对象和new T()的对象相同,则不会保存数据到对应xml。
  • T被称为State
  • 可以使用@com.intellij.util.xmlb.annotations.Transient注解字段不存储。
  • T 必须有默认构造方法,返回默认对象。用于判断是否需要存储数据。应该要有自定义equals方法,如果没有,则会根据State对象的属性来判断。
  • 支持序列化的类型:
    1. 数字及其包装类,boolean,String,collection,map,enum
    2. 其他类型必须使用对应的转换器:

2.1 实现自定义converter,继承com.intellij.util.xmlb.Converter

class LocalDateTimeConverter extend Converter<LocalDateTime>{
    public LocalDateTime fromString(String value) {
    final long epochMilli = Long.parseLong(value);
    final ZoneId zoneId = ZoneId.systemDefault();
    return Instant.ofEpochMilli(epochMilli).atZone(zoneId).toLocalDateTime();
  }

  public String toString(LocalDateTime value) {
    final ZoneId zoneId = ZoneId.systemDefault();
    final long toEpochMilli = value.atZone(zoneId).toInstant().toEpochMilli();
    return Long.toString(toEpochMilli);
  }
}

2.2 在属性字段上加注解:

class State {
  @OptionTag(converter = LocalDateTimeConverter.class)
  public LocalDateTime dateTime;
}

使用示例:

  1. 继承PersistentStateComponent并定义State类
@State(
      // 存储xml标签信息
        name = "bx-config",
        storages = {
              // 存放的文件名
                @Storage("bx-tools.xml")
        }
)
public class MyProjectServiceImpl implements IMyProjectService, PersistentStateComponent<MyProjectServiceImpl.MyConfig> {
    // static必须,否则ideaa无法创建
    static class MyConfig {
        // public属性或带有注解
        public String abstractTestService;
    }
    // 应该是可以不new的,load时会创建默认赋值
    private MyConfig projectConfig = new MyConfig();
    private Project project;

    public MyProjectServiceImpl(Project project) {
        this.project = project;
    }

    @Override
    public String getAbstractTestService() {
        return projectConfig.abstractTestService;
    }

    @Override
    public void resetAbstractTestService(String abstractTestService) {
        projectConfig.abstractTestService = abstractTestService;
    }

    @Nullable
    @Override
    public MyConfig getState() {
        return projectConfig;
    }

    @Override
    public void loadState(@NotNull MyConfig o) {
        projectConfig = o;
    }
}
  1. 定义Service:
<idea-plugin>
    <extensions defaultExtensionNs="com.intellij">
   <projectService serviceImplementation="com.zhu.intellij.plugin.service.projrct.impl.MyProjectServiceImpl"
                    serviceInterface="com.zhu.intellij.plugin.service.projrct.IMyProjectService" />
    </extensions>
</idea-plugin>
  1. 查看保存的数据:
    文件位置:因为配置的是Project Level Service,所以存储的是在:项目目录/.idea/bx-tools.xml
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="bx-config">
    <option name="abstractTestService" value="xxx"/>
  </component>
</project>

扩展阅读:

Action 和 ActionGroup

Action

action系统是intellij platform提供增加自己的选项到菜单栏或工具栏的一种扩展点。action需要实现代码及注册。action代码实现中决定再哪些地方会显示你的选项以及选中之后的功能实现。

  • 创建action:
    最好用的是下面的方式,能帮我们自动配置plugin.xml


    1.png

这种方式创建还有个好处是可以选择放入的ActionGroup:


2.png
  • 也可以手动创建action实现类,自己配置plugin.xml:
  • 实现一个Action:
    1. 继承AnAction
    2. 重写update和actionPerformed方法:
      update方法控制,当前上下文是否显示及选项是否可用。
      actionPerformed:实现用户选中你的选线之后的操作。
      以上的两个方法都有一个参数:AnActionEvent,可以从中获取Project,file, selection等信息。
  • 配置一个action:
<idea-plugin>
  <actions>
    <!-- id全局唯一 -->
    <action id=“zhu.dubbo_test_code" 
            <!-- action实现类 -->
            class="com.zhu.intellij.plugin.action.DubboTestCodeAction" 
            <!-- 选项显示文案 -->    
            text="generate dubbo test code" 
            description="generate dubbo test code">
      <!-- 放入的ActionGroup -->
        <add-to-group group-id="EditorPopupMenu" anchor="last"/>
     </action>                             
  </actions>
</idea-plugin>   
  • 代码中注册一个Action:
// 创建action
DubboTestCodeAction action = new DubboTestCodeAction();
// 注册action
ActionManager.getInstance().registerAction("zhu.myAction", action);
// 获取放入的action group
DefaultActionGroup actionGroup = (DefaultActionGroup)ActionManager.getInstance().getAction("EditorPopupMenu");
// action放入action group
actionGroup.addAction(action);

Action Group

action组,类似Profile就是一个Action Group。其本身也放在了Run的action group中。

  • 自定义一个action group:

    1. 直接在plugin.xml中通过<group>标签创建
    2. 继承DefaultActionGroup:
    3. 继承ActionGroup:可以实现可变的action集合,上面的Profile就是一个可变的,子action包含可变信息的。
      • update:控制action group是否展示及可用
      • getChildren:动态返回action列表。也可以包含action group。
  • 如何找到想要放入的ActionGroup:

    • 创建action步骤中的第二步,可以通过关键词搜索ActionGroup,参照右边显示的action来确定是否是想要的。


      3.png
    • 常用的ActionGroup:
      EditorPopupMenu->编辑器窗口中右键
      ProjectViewPopupMenu->项目导航栏中右键
      ToolsMenu->菜单栏上的Tools菜单

每个action和action group都需要有一个唯一的ID。

Setting

持久化模型+UI实现界面化设置。 // 只做演示,不做具体讲解。官方示例中的settings示例

File

Virtual File System:

虚拟文件系统。简称VFS。是Interllij平台的一个组件,封装了文件相关操作到Virtual File.

  • 提供一下几种能力:
    • 提供了通用的文件处理API,而不需要关心文件存储的位置。(磁盘上、HTTP服务器上)
    • 当检测到文件内容被修改时,能够追踪文件修改并提供修改前后的文件内容。
    • 提供将附加持久数据与VFS中的文件关联的可能性。
      为了提供最后两个特性,VFS管理一些用户硬盘内容的持久快照。快照只存储那些通过VFS API至少请求过一次的文件,并进行异步更新以匹配磁盘上发生的更改。快照存储是application级别的。

Virtual File:

  • 如何获取Virtual File:
    • action中的:AnActionEvent.getData(PlatformDataKeys.VIRTUAL_FILE);多选的时候,使用AnActionEvent.getData(PlatformDataKeys. VIRTUAL_FILE_ARRAY);
    • 本地文件系统获取:LocalFileSystem.getInstance().findFileByIoFile() 或 VirtualFileManager.findFileByNioPath()
    • psiFile获取对应的virtual file:psiFiel.getVirtualFile(),当psiFile还只是在内存中创建是,该方法可能返回null。
    • Document对应的Virtual File:FileDocumentManager.getInstance().getFile(Document doc)
  • Virtual File可以执行哪些操作:
    • 遍历文件系统,获取文件内容,文件重命名,移动,删除等。
      递归迭代应该使用VfsUtilCore.iterateChildrenRecurse()来执行,以防止由递归符号链接引起的无穷循环。

VFS是从项目根目录开始,通过上下扫描文件系统以增量方式构建的。VFS刷新检测文件系统中出现的新文件。可以使用VirtualFileManager.syncRefresh()/ asyncRefresh()或VirtualFile.refresh()以编程方式初始化刷新操作。当文件系统监视程序收到文件系统更改通知时,也会触发VFS刷新。要访问刚刚由外部工具通过IntelliJ平台api创建的文件,可能需要调用VFS刷新。

  • 创建Virtual File:
    • virtualFile.createChildData() 创建同目录的virtual file。
    • virtualFile.setBinaryContent()设置文件内容。
  • 监听VFS的变化:
project.getMessageBus().connect().subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener() {
    @Override
    public void after(@NotNull List<? extends VFileEvent> events) {
        // handle the events
    }
});
  • 相关的工具类:
    • VfsUtil和VfsCoreUtil:virtual fiel相关操作
    • ProjectLocator: 查找包含该virtual file的project
  • 如何扩展VFS:
    • 继承VirtualFileSystem,通过扩展点:com.intellij.virtualFileSystem配置。

Document

概念:Document是一个可编辑的Unicode字符序列,通常对应于虚拟文件的文本内容。

  • 如何获取Document:
    • 从action中获取:e.getData(CommonDataKeys.EDITOR).getDocument()
    • 获取virtual file获取对应的document:FileDocumentManager.getDocument(virtualFile),如果是确定已加载的,可以使用FileDocumentManager.getCachedDocument()
    • 获取psiFile获取对应的document:PsiDocumentManager.getInstance().getDocument(psiFile) 或 PsiDocumentManager.getInstance().getCachedDocument(psiFile)
  • Document能执行哪些操作:
    • 在纯文本层级的字符序列去操作和修改文件内容。
  • 相关工具类:
    • DocumentUtil

Editor

概念:编辑器窗口对应的对象。

  • 获取选中的编辑器窗口:

    • 从AnActionEvent中获取:e.getData(CommonDataKeys.EDITOR);
    • 如果有DataContext对象,通过CommonDataKeys.EDITOR.getData(dataContext);
    • 如果有project对象,可以通过FileEditorManager.getInstance(project).getSelectedTextEditor()
  • 可以获取Editor中不同的文本表现模型。

    • SelectionModel: 选中的文本
    • MarkupModel:操作文档内容的高亮显示等。
    • FoldingModel:文档的折叠模型,该模型可用于添加、删除、展开或折叠文档中的折叠区域。
    • ScrollingModel:文档的滚动模型,该模型可用于滚动文档并检索关于滚动条当前位置的信息。
    • 其他等。
  • 示例:action中替换选中文本:

public class EditorIllustrationAction extends AnAction {
  @Override
  public void actionPerformed(@NotNull final AnActionEvent e) {
    // Get all the required data from data keys
    final Editor editor = e.getRequiredData(CommonDataKeys.EDITOR);
    final Project project = e.getRequiredData(CommonDataKeys.PROJECT);
    final Document document = editor.getDocument();

    // Work off of the primary caret to get the selection info
    Caret primaryCaret = editor.getCaretModel().getPrimaryCaret();
    int start = primaryCaret.getSelectionStart();
    int end = primaryCaret.getSelectionEnd();

    // Replace the selection with a fixed string.
    // Must do this document change in a write action context.
    WriteCommandAction.runWriteCommandAction(project, () ->
        document.replaceString(start, end, "editor_basics")
    );

    // De-select the text range that was just replaced
    primaryCaret.removeSelection();
  }
}
  • 坐标系统:

    • 逻辑位置:行号和列号从0开始,插入符号的逻辑位置行号将忽略更改编辑器中文档显示方式的设置的效果。这些设置的例子是代码(线)折叠和软线缠绕。这些效果意味着,无论编辑器中的一行或多行是折叠还是软包装,插入符号逻辑位置行号都不会改变。通过editor.getCaretModel().getLogicalPosition()获取。
    • 视觉位置:插入符号的视觉位置不同于逻辑位置,因为它考虑了编辑器表示设置,如代码折叠和软换行。在此过程中,VisualPosition计算可在编辑器中显示的文档行数(以零为基础)。因此,视觉位置不能唯一地映射到逻辑位置或底层文档中相应的行。通过editor.getCaretModel().getVirtualPostio()获取。
    • 列位置: 从0开始计数,举例本行最开始的位置,其中tab算多少个,根据设置而定。
    • offset: 距离文档开始的偏移量。editor.getCaretModel().getOffset()
  • 文本选中的扩展点:

    • 扩展点:com.intellij.extendWordSelectionHandler
    • 实现接口:ExtendWordSelectionHandler
    • 功能:在你的plugin.xml中实现ExtendWordSelectionHandler并将其注册为com.intellij.extendWordSelectionHandler EP允许你在扩展或收缩选择时提供额外的文本范围。从canSelect(PsiElement)返回true,用于为您想要提供额外文本范围的PSI元素。IntelliJ平台会为这些元素调用select(PsiElement, CharSequence, int, Editor),在这里你可以计算额外的文本范围并以List<TextRange>的形式返回它们。

Project Model

一个idea项目包含Project, Module, Library,Facet,SDK等。

  • Project:在IntelliJ平台中,项目将项目的所有源代码、库和构建指令封装到单个组织单元中。使用IntelliJ平台SDK所做的一切都是在项目的上下文中完成的。项目定义了称为模块和库的集合。根据项目的逻辑和功能需求,您可以创建单模块或多模块项目。
  • Module:模块是可以独立运行、测试和调试的功能单元。模块包括源代码、构建脚本、单元测试、部署描述符等。在项目中,每个模块可以使用特定的SDK,也可以继承在项目级别定义的SDK(请参阅本文档下面的SDK部分)。一个模块可以依赖于项目的其他模块。
  • Library:库是模块所依赖的已编译代码(如JAR文件)的归档文件。IntelliJ平台支持三种类型的库:
    • 模块库:库类只在这个模块中可见,并且库信息记录在模块的.iml文件中。
    • 项目库:库类在项目中可见,库信息记录在项目的.ipr文件或.idea/libraries中。
    • 全局库:库信息记录在<User Home>/.IntelliJIdea/config/options目录下的applicationLibraries.xml文件中。全局库类似于项目库,但是对于不同的项目是可见的。
  • SDK: 每个项目都使用软件开发工具包(SDK)。对于Java项目,SDK被称为JDK (Java开发工具包)。SDK决定使用哪个API库来构建项目。如果一个项目是多模块的,默认情况下,项目SDK对该项目中的所有模块都是通用的。项目还可以为每个模块配置单独的SDK。
  • Facet: facet表示特定于与模块相关联的特定框架/技术对应的配置。一个模块可以有多个Facet。例如,特定于Spring的配置存储在Spring Facet中。

Project

  • 项目相关的设置都保存在各自项目的.idea目录下
  • 和Project功能相关的类有:
    • Project:对应一个project
    • ProjectRootManaer: 获取本项目的所有model及project相关的根目录,源数据目录等。
    • ProjectManager:管理所有project,提供加载,创建,打开project功能。
    • ProjectFileIndex:管理project中的virtual file信息,可以获取某个virtual file所属的Model,对应的内容根目录,源根目录等。isLibraryXxxx()用来判断对应virtual file是否属于Library
  • 监听项目结构变化事件:
    • topic:ProjectTopics.PROJECT_ROOTS
    • interfaceClass:com.intellij.openapi.roots.ModuleRootListener
  • 扩展阅读:扩展创建项目中的一些步骤

Module

  • Module中的关键组件:
    • Content Roots:内容根目录,标记哪些目录中的文件属于该Module,一个目录只能属于一个Module。

    • Source Roots:源根目录。一个内容根目录能包含多个源根目录。源根目录有不同的类型:常规源根目录,测试源根目录和资源根目录等(对应目录右键中Mark Directroy as中的选项)。常规源根目录中的代码不能依赖测试源根目录中的代码,反之可以。

    • Order entries:有顺序的Module的依赖,依赖可以试试SDK,Library或者其他Module。

    • Facets:特定框架相关的配置项列表

  • 和Module相关功能类:
    • Module:module本体
    • ModuleUtil:获取project中特定ModuleType的Module列表
    • ModuleManager:对Module的管理,提供创建新Module,加载Module,销毁Module等功能。
    • ModuleRootManager:提供获取本Module依赖的其他Module,及本Module对应的ModuleFileIndex对象。
    • 其他等等。
  • 监听Module相关的变化事件:
    • topic:ProjectTopics.MODULES
    • listenerClass:com.intellij.openapi.project.ModuleListener
  • 相关的其他使用方式

SDK,Library, Facet等:

不做过多介绍,参见:官网文章-SDK, 官网文章-Library官网文章-Facet

PSI

程序结构接口(Program Structure Interface),通常简称为PSI,是IntelliJ平台中负责解析文件和创建语法和语义代码模型的层,该模型支持平台的许多特性。

PsiFile

PSI(程序结构接口)文件是一个结构的根,该结构将文件的内容表示为一种特定编程语言中的元素层次结构。

所有Psi file的基类是PsiFile类,不同的语言有不同的子类,比如JavaPsiFile表示一个java文件,XmlFile表示一个xml文件。
不像VirtualFile和Document,它们有应用范围(即使多个项目是打开的,每个文件都用同一个VirtualFile实例表示),PSI有项目范围:如果文件属于多个同时打开的项目,则同一个文件用多个PsiFile实例表示。

  • 如何获取PsiFile:
    • 在action中获取:anActionEvent.getData(CommonDataKeys.PSI_FILE)
    • 从对应virtualFile获取:PsiManager.getInstance(project).findFile(virtualFile);
    • 获取对应document的psiFile:PsiDocumentManager.getInstance(project).getPsiFile(document)
    • 从psiFile中的任意psiElement获取:psiElement.getContainingFile()
    • 通过名称搜索:FilenameIndex.ggetFilesByName(project, name, scope)
      PsiFile能用来干什么:
    • 访问和修改其中的元素
  • 如何创建PsiFile:
    分为两步:
    • 在内存中创建一个PsiFile:PsiFactory.createFileFromText(fileName, fileType, fileContent)
    • 保存到硬盘:放到某个目录下-psiDirectory.add(psiFile). tips: 该方法会返回一个PsiFile,之后的代码请用这个返回的PsiFile。
  • 如何监听PsiFile的变化事件:
    有两种方式:
    • PsiManager.getInstance(project).addPsiTreeChangeListener()注册一个listener
    • 使用com.intellij.psi.treeChangeListener的扩展点配置PsiTreeChangeListener。

FileViewProvider

文件视图提供程序(FileViewProvider)管理对单个文件中的多个PSI树的访问。
例如,一个JSP页面有一个单独的用于其中的Java代码的PSI树(PsiJavaFile),一个单独的用于XML代码的树(XmlFile),以及一个单独的用于JSP作为一个整体的树(JspFile)。

每个PSI树覆盖文件的全部内容,并在可以找到不同语言的内容的地方包含特殊的“外部语言元素”。

一个FileViewProvider实例对应一个VirtualFile,一个Document,并且可以检索多个PsiFile实例。

  • 如何获取FileViewProvider:
    • 从virtualFile获取:PsiManager.getInstance(project).findViewProvider(virtualFile)
    • 从psiFile获取:psiFile.getViewProvider()
      FileViewProvider可以用来干什么:
    • 获取当前psi树中使用到的所有语言:fileViewProvider.getLanguages();
    • 获取特定语言的PsiFile: fileViewProvider.getPsi(language)
    • 获取指定offset和语言的psiElement: fileViewProvider.findElementAt(offset, language)

PsiElement

一个PSI文件代表一个PSI元素的层次结构(所谓的PSI树)。一个PSI文件(本身是一个PSI元素)可能包含几个特定编程语言的PSI树。一个PSI元素,反过来,可以有子PSI元素。

PSI元素和单个PSI元素级别的操作被用于探索由IntelliJ平台解释的源代码的内部结构。例如,您可以使用PSI元素来执行代码分析,例如代码检查或意图操作。

  • 如何获取到PsiElement:
    • 在action中:anActionEvent.getData(LangDataKeys.PSI_ELEMENT)
    • 通过offset在psiFile中获取对应位置的psiElement:psiFile.findElementAt(offset), 返回的是所在位置最小psiElement单元,如果想获取你想要的类型,可以使用PsiTreeUtil.getParentOfType()获取。
    • 使用PsiRecursiveElementWalkingVisitor遍历psiFIle
    • 通过解析引用:PsiReference.resolve()

Psi Reference

PSI树中的引用是一个对象,它表示从代码中特定元素的使用到相应声明的链接。解析引用意味着定位特定用法所引用的声明。

引用是实现PsiReference接口的类的实例。注意,引用不同于PSI元素。由PSI元素创建的引用从PsiElement.getReferences()返回,引用的基础PSI元素可以从PsiReference.getElement()获得,所引用的声明可以从PsiReference.resolve()获得。

PSI导航

  • psi中导航有三种方式:从上往下,行下网上和引用。
    • 从上往下:使用psiElement.accep() 访问子元素。或者使用PsiTreeUtil.findChildOfType()。
    • 从下往上:使用PsiTreeUtil.getParentOfType(element, PsiMethod.class) 获取父元素。
    • 引用:PsiElement.getReferences()获取引用信息。

Psi文件结构解析

设置方式:idea.properties中增加idea.is.internal=true后重启,就能在Tools菜单栏下找到View PSI Structure选项。教程: 配置教程

  • java类:
package pack;
import abc;
public class A extends B implements C,D{
    private String str;
    public void hello(String message) {
        System.out.println(message);
    }
}
  • Psi的层级结构:
PsiJavaFile:Dummy.java(0,176)
  PsiPackageStatement:pack(0,13)
    PsiKeyword:package('package')(0,7)
    PsiWhiteSpace(' ')(7,8)
    PsiJavaCodeReferenceElement:pack(8,12)
      PsiIdentifier:pack('pack')(8,12)
      PsiReferenceParameterList(12,12)
        <empty list>
    PsiJavaToken:SEMICOLON(';')(12,13)
  PsiWhiteSpace('\n')(13,14)
  PsiImportList(14,25)
    PsiImportStatement(14,25)
      PsiKeyword:import('import')(14,20)
      PsiWhiteSpace(' ')(20,21)
      PsiJavaCodeReferenceElement:abc(21,24)
        PsiIdentifier:abc('abc')(21,24)
        PsiReferenceParameterList(24,24)
          <empty list>
      PsiJavaToken:SEMICOLON(';')(24,25)
  PsiWhiteSpace('\n')(25,26)
  PsiClass:A(26,175)
    PsiModifierList:public(26,32)
      PsiKeyword:public('public')(26,32)
    PsiWhiteSpace(' ')(32,33)
    PsiKeyword:class('class')(33,38)
    PsiWhiteSpace(' ')(38,39)
    PsiIdentifier:A('A')(39,40)
    PsiTypeParameterList(40,40)
      <empty list>
    PsiWhiteSpace(' ')(40,41)
    PsiReferenceList(41,50)
      PsiKeyword:extends('extends')(41,48)
      PsiWhiteSpace(' ')(48,49)
      PsiJavaCodeReferenceElement:B(49,50)
        PsiIdentifier:B('B')(49,50)
        PsiReferenceParameterList(50,50)
          <empty list>
    PsiWhiteSpace(' ')(50,51)
    PsiReferenceList(51,65)
      PsiKeyword:implements('implements')(51,61)
      PsiWhiteSpace(' ')(61,62)
      PsiJavaCodeReferenceElement:C(62,63)
        PsiIdentifier:C('C')(62,63)
        PsiReferenceParameterList(63,63)
          <empty list>
      PsiJavaToken:COMMA(',')(63,64)
      PsiJavaCodeReferenceElement:D(64,65)
        PsiIdentifier:D('D')(64,65)
        PsiReferenceParameterList(65,65)
          <empty list>
    PsiJavaToken:LBRACE('{')(65,66)
    PsiWhiteSpace('\n    ')(66,71)
    PsiField:str(71,90)
      PsiModifierList:private(71,78)
        PsiKeyword:private('private')(71,78)
      PsiWhiteSpace(' ')(78,79)
      PsiTypeElement:String(79,85)
        PsiJavaCodeReferenceElement:String(79,85)
          PsiIdentifier:String('String')(79,85)
          PsiReferenceParameterList(85,85)
            <empty list>
      PsiWhiteSpace(' ')(85,86)
      PsiIdentifier:str('str')(86,89)
      PsiJavaToken:SEMICOLON(';')(89,90)
    PsiWhiteSpace('\n    ')(90,95)
    PsiMethod:hello(95,173)
      PsiModifierList:public(95,101)
        PsiKeyword:public('public')(95,101)
      PsiTypeParameterList(101,101)
        <empty list>
      PsiWhiteSpace(' ')(101,102)
      PsiTypeElement:void(102,106)
        PsiKeyword:void('void')(102,106)
      PsiWhiteSpace(' ')(106,107)
      PsiIdentifier:hello('hello')(107,112)
      PsiParameterList:(String message)(112,128)
        PsiJavaToken:LPARENTH('(')(112,113)
        PsiParameter:message(113,127)
          PsiModifierList:(113,113)
            <empty list>
          PsiTypeElement:String(113,119)
            PsiJavaCodeReferenceElement:String(113,119)
              PsiIdentifier:String('String')(113,119)
              PsiReferenceParameterList(119,119)
                <empty list>
          PsiWhiteSpace(' ')(119,120)
          PsiIdentifier:message('message')(120,127)
        PsiJavaToken:RPARENTH(')')(127,128)
      PsiReferenceList(128,128)
        <empty list>
      PsiWhiteSpace(' ')(128,129)
      PsiCodeBlock(129,173)
        PsiJavaToken:LBRACE('{')(129,130)
        PsiWhiteSpace('\n        ')(130,139)
        PsiExpressionStatement(139,167)
          PsiMethodCallExpression:System.out.println(message)(139,166)
            PsiReferenceExpression:System.out.println(139,157)
              PsiReferenceExpression:System.out(139,149)
                PsiReferenceExpression:System(139,145)
                  PsiReferenceParameterList(139,139)
                    <empty list>
                  PsiIdentifier:System('System')(139,145)
                PsiJavaToken:DOT('.')(145,146)
                PsiReferenceParameterList(146,146)
                  <empty list>
                PsiIdentifier:out('out')(146,149)
              PsiJavaToken:DOT('.')(149,150)
              PsiReferenceParameterList(150,150)
                <empty list>
              PsiIdentifier:println('println')(150,157)
            PsiExpressionList(157,166)
              PsiJavaToken:LPARENTH('(')(157,158)
              PsiReferenceExpression:message(158,165)
                PsiReferenceParameterList(158,158)
                  <empty list>
                PsiIdentifier:message('message')(158,165)
              PsiJavaToken:RPARENTH(')')(165,166)
          PsiJavaToken:SEMICOLON(';')(166,167)
        PsiWhiteSpace('\n    ')(167,172)
        PsiJavaToken:RBRACE('}')(172,173)
    PsiWhiteSpace('\n')(173,174)
    PsiJavaToken:RBRACE('}')(174,175)
  PsiWhiteSpace('\n')(175,176)

扩展阅读

参考链接

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

推荐阅读更多精彩内容