Idea插件开发入门

2021-09-21  本文已影响0人  换个名字_b5dd

内容范围

本文档旨在介绍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. 插件安装后目录结构:
.IntelliJIDEAx0/
└── plugins
    └── sample.jar
        ├── com/foo/...
        │   ...
        │   ...
        └── META-INF
            ├── plugin.xml
            ├── pluginIcon.svg
            └── pluginIcon_dark.svg
.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);
    }
  }
}

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()方法。

<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,优势在于声明式只会在对应事件第一次触达的时候才会构造实例。

<idea-plugin>
    <applicationListeners>
      <listener class="myPlugin.MyListenerClass" topic="BaseListenerInterface"/>
    </applicationListeners>
</idea-plugin>
- topic:listener感兴趣的event。通常是event对应topic实例的listenerClass。
- class : 接受event的实现类。
messageBus.connect().subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener() {
    @Override
    public void after(@NotNull List<? extends VFileEvent> events) {
        // handle the events
    }
});
<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
    }
}
<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

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

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()方法去释放资源。

线程和消息架构

线程:

详细说明参考:官网文章

  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是否还有效。

消息传递机制:

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

核心概念:

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

使用说明:

  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.
        }
    });
}
<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

用户交互组件

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

持久化模型

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

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代码实现中决定再哪些地方会显示你的选项以及选中之后的功能实现。

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


2.png
<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
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和action group都需要有一个唯一的ID。

Setting

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

File

Virtual File System:

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

Virtual File:

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

project.getMessageBus().connect().subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener() {
    @Override
    public void after(@NotNull List<? extends VFileEvent> events) {
        // handle the events
    }
});

Document

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

Editor

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

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();
  }
}

Project Model

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

Project

Module

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实例表示。

FileViewProvider

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

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

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

PsiElement

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

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

Psi Reference

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

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

PSI导航

Psi文件结构解析

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

package pack;
import abc;
public class A extends B implements C,D{
    private String str;
    public void hello(String message) {
        System.out.println(message);
    }
}
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)

扩展阅读

参考链接

上一篇 下一篇

猜你喜欢

热点阅读