Bytedance Terminal Technology — Chen Tao Zhou
An overview of the
The purpose of this article is to introduce the reader to the IntelliJ IDE plug-in development process and some common functions commonly used. Any IDE developed based on IntelliJ can be made by this method, such AS Android Studio (AS), This article will also be based on Android Studio, the reader will learn Android Studio plug-in development from 0 to 1.
background
What are IDE plug-ins and what can IDE plug-ins do?
IDE plug-in integrates some functions into IDE interface. When we use IDE for development work, we can easily use these functions through UI interface, such as the familiar Project directory, Gradle toolbar, Run, Terminal, Build interface at the bottom of IDE, etc. All are implemented through IDE plug-ins, so it can be said that most of the operations that need to be done through the command line or the user manual can be implemented through plug-ins and presented as a UI.
On the left, the Gradle toolbar on the right side of the Android Studio IDE interface contains many Gradle tasks. Clicking on the UI has the same effect as typing Gradle commands on the command line. The figure on the right is the version control part of the IDE top menu bar, in which buttons for version submission and pull are equivalent to command line input corresponding instructions.
Why make an Android Studio IDE plug-in?
As a developer in the mid-taiwan department, THE author is often involved in the development of some common functions and hand them over to external use in the form of tools or components. Therefore, how to reduce user learning cost and improve work efficiency is my goal, and these optimization directions are inseparable from the clever use of tools. For example, the IDE plug-in development background to be introduced this time is aimed at this: the work that originally needs to be completed by using the command line, or the operation with high learning cost, is packaged through UI and attached to the native AS interface. Through UI interaction, the user’s learning cost is greatly reduced and the user experience is improved.
For example, compare the following two pictures is a project of automatic scaffolding screenshot, about two figure respectively using the command line and use AS a plug-in experience contrast, you can see on the left side when using the CLI command line for engineering structures, information on the interface is not simple, and user interaction experience is poorer, the user must read the document before use, and there is no fault tolerance mechanism, If you make a mistake, you have to start from scratch. And the experience of the same function on the right side of the AS plug-ins is a lot of good, not only individual information clear, can also expand more detail function, such AS dynamic inspection user input, the input is correct to take the next step, etc., the user can completely in the case of zero knowledge background using the plug-in and easy to complete all function operation, and close to the native interface is more beautiful.
How to develop an IDE plug-in?
The preparatory work
Before developing the first plug-in, we need to download the correct development tools. Download IntelliJ IDEA from the JetBrain website.
The development tool used here is IntelliJ IDEA rather than Android Studio, because AS is based on the IntelliJ template, IDE plug-ins must be developed, distributed, and installed into Android Studio to use.
We need to confirm which IntelliJ version of Android Studio we are using. This is important, AS the same version of Android Studio that you are currently using will allow you to debug easily, while the new version of IntelliJ includes features that may not be available on your AS, causing the plug-in to fail to install or causing compatibility errors. (The error in the figure can also occur when compatibility with higher versions is not enabled, as required during development).
Please follow this step when downloading:
- Open your Android Studio and look at The Build Number. This is The IntelliJ version we need.
- On the download page, click Other Versions to find the corresponding IntelliJ version, download and install it.
Create a new project + configuration
This part also can refer to website: www.jetbrains.org/intellij/sd…
- To create a new project, there are two wizard pages: [Select the project template framework + fill in the plug-in project information], which can be configured according to the figure.
- After completing the two-step wizard to automatically create the project, we need to know the two core files [build.gradle + plugin.xml] and do some pre-configuration work.
build.gradle
Since build.gradle is very similar to the build files in Android projects, only the configurations not available in Android are explained here.
- Version: The intellij closure is created with only one attribute, version. This attribute represents the version of the Intellij platform IDE used to build the plugin. If we call the Gradle task [runIde] at development time, An IntelliJ IDE instance based on this version will be created.
- LocalPath: Since we want to test our plugin under AS, we need to make AS a dependency of our plugin and add a property called localPath to specify the installation directory of the native Android Studio application Contents. An Instance of Android Studio based on this version will be created (note that localPath cannot be used with the Version attribute, since version information is already in our local AS path).
- Plugins: Add dependent plug-ins needed for development. There are many plugins that can be added here, such as’ git4idea ‘plugin if you want to execute git commands in your plugin.
intellij {
version '2020.1.4'
localPath '/Applications/Android Studio.app/Contents'
plugins = ['Kotlin'.'android'.'git4idea']}Copy the code
plugin.xml
Plugin. XML file can be found in the resource folder, which can configure various attributes of our plug-in. The core function is to register the components and service contained in our plug-in (after the function class is implemented, it needs to be registered here to use it. Similar to declaring activities and services in androidmanifest.xml).
- Declare that our plugin needs and is compatible with AS: Add Android and Android Studio Modules AS dependencies.
<idea-plugin>.<depends>com.intellij.modules.platform</depends>
<depends>org.jetbrains.android</depends>
<depends>com.intellij.modules.androidstudio</depends>
<extensions defaultExtensionNs="com.intellij">
<! -- Add your extensions here -->
</extensions>
<actions>
<! -- Add your actions here -->
</actions>
</idea-plugin>
Copy the code
Run the plugin
After the configuration is complete, we can try to run the plugin project from the Gradle toolbar project name /Tasks/intelliJ/runIde path. Run the runIde task, and since we configured Android Studio as the startup path, an Android Studio mock IDE will open, and everything will be the same as our native Android Studio.
IDE plug-in common functions
Create an Action
What is the Action?
Official introduction of Actions: The system of actions allows plugins to add their own items to IDEA menus and toolbars. An action is a class, derived from the AnAction.
Actions is the most common way for users to invoke plug-in functions. The tools directory shown in the figure below is often used by developers. All options in this directory are an Action, and Action groups can be expanded further.
How do I create an Action?
Two steps:
- 【code implementation – implementation of the Action of the specific code logic 】 : Determines the context in which the action is valid and the UI’s selected functionality (inheriting the parent AnAction and overriding the actionPerformed() method for the callback after the action is executed).
- [Registered – Registered in a configuration file] : Determines where in the IDE the action will appear (create a new group or store it in an existing ActionGroup, and where in the group).
When the two conditions are met, the action gets a callback from the IntelliJ Platform after the user has performed the action, for example: ‘HelloWorld’.
Code implementation
class HelloWorldAction : AnAction() {
override fun actionPerformed(event: AnActionEvent) {
// Create a message prompt popup to display "Hello World" in the IDE
val notificationGroup = NotificationGroup(
displayId = "myActionId",
displayType = NotificationDisplayType.BALLOON
)
val notification = notificationGroup.createNotification(
title = "chentao Demo",
content = "Hello World",
type = NotificationType.INFORMATION
).notify(event.project) // Get the project currently being displayed by the IDE from the Event object of the method and display the popover in the project}}Copy the code
Registering a Custom Action
<actions>
<! -- Add your actions here -->
<! Create an ActionGroup -->
<group id = "ChentaoDemo.TopMenu"
text="ChentaoDemo Plugin"
description="Demo Plugin in top menu">
<! HelloWorld Action -->
<action class="com.chentao.demo.actions.HelloWorldAction"
id="DemoAction"
text="Hello World Action"
description="This is a test action">
<! -- Set keyboard shortcut for HelloWorld Action -->
<keyboard-shortcut first-keystroke="control alt p" keymap="$default"/>
<! Add HelloWorld Action to the clip-copy group -->
<add-to-group group-id="CutCopyPasteGroup" anchor="last"/>
</action>
<! Add this Group to main menu -->
<add-to-group group-id="MainMenu" anchor="last"/>
</group>
</actions>
Copy the code
Run the plug-in – results show
After implementing the above two steps, run runIde Task, the ActionGroup we added appears at the end of the main menu bar at the top, expand it and see HelloWorldAction, click Action, and the prompt message “HelloWorld” pops up in the lower right corner. Not only can we create groups to place actions, we can also add actions to existing groups in the IDE. In the image on the left below, we add the HelloWorld Action to the IDE’s CutCopyPasteGroup, along with the copy and paste actions.
In plugin.xml, actions groups can be more complex. Groups can be included with each other and form toolbars or menus (see figure below). If you are interested, you can pull up a Demo(at the end of this article) to try it out.
Wizard program Wizard
Wizard refers to a program that guides users to complete a function. It is usually composed of one or more guide interfaces. For example, the two images below show the classic Create a new project window in Android Studio, which contains a two-page wizard. Here’s how to create a wizard that looks exactly like the theme shown in the figure.
The base class of the wizard belongs to Android.jar and consists of the following core classes:
1. ModelWizard
The “main class” of the wizard, where a ModelWizard contains a queue of ModelWizardsteps (a Step queue is an ordered queue, and each Step contains a reference to its next Step), and when a ModelWizard finishes, it traverses all the steps, Access the WizardModel corresponding to step and call the WizardModel#handleFinished() method.
2. ModelWizardStep
A Step is a single page in the Wizard program, which is responsible for creating a UI interface to present to the user, determining whether the information on the page is valid, and storing the user data in the corresponding WizardModel object.
3. SkippableWizardStep
Steps that can be set with visibility can be used to control the visibility of the Steps that follow. For example, a Step provides options for the user to choose which Steps can be displayed.
4. WizardModel
A Model is a collection of data populated by each step in the Wizard. Multiple steps can share the same Model, and the core method is handleFinished(), which is called for the final logical processing when the wizard ends when the user clicks “Finish” in the wizard.
Wizard Workflow flowchart
Create an Android Studio-style wizard
Also in the Android. jar library, a package called UI, the wizard’s equivalent, provides a handy class to help users create as-style ModelWizards. Simply ModelWizard object as a parameter in the constructor StudioWizardDialogBuilder. Wrapping our plug-in UI in AS style gives users a more native feel and a better experience.
class CreateNewProjectAction : AnAction() {
override fun actionPerformed(e: AnActionEvent) {
StudioWizardDialogBuilder(
ModelWizard.Builder().addStep(NewProjectStep()).build(),
"Create New MARS Project"
).build().show()
}
}
class ProjectWizardModel : WizardModel() {
// Record some fields that you want to save
/ /...
override fun handleFinished(a) {
// Handle the final logic}}class NewProjectStep : ModelWizardStep<ProjectWizardModel?>(ProjectWizardModel(), "Create MARS Project") {
init {
// Create the Step page UI
}
// Link to the next Step
override fun createDependentSteps(a): MutableCollection<out ModelWizardStep<*>> {
return arrayListOf(SelectBaselineStep(model))
}
}
Copy the code
Tool Windows
Tool Windows is a child window of the IDE. These Windows usually have their own Tool Window button on the “border” of the IDE main window. Clicking on the button will activate the panel to display information on the left, right and bottom sides of the IDE main window. The Gradle toolbar on the left is shown below. To create a ToolWindow, provide a JPanel and implement it via ToolWindowFactory.
ToolWindowFactory
Performs lazy initialization of a tool window registered in {@code plugin.xml}.
The user must create an implementation class for ToolWindowFactory and implement the createToolWindowContent() method, which initializes the TOOL Window UI and adds it to Android Studio. ToolWindowFactory provides a lazy loading mechanism, which is implemented with the benefit that unused tool Windows do not increase startup time or incur any overhead in memory usage: if the user does not interact with the tool window, the relevant code will not be loaded and executed.
public class MyToolWindowFactory implements ToolWindowFactory {
@Override
public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
// Initializes the custom component object
MyToolWindow myToolWindow = new MyToolWindow(project, toolWindow);
// Add the component to the AS
ContentFactory contentFactory = ContentFactory.SERVICE.getInstance();
Content content = contentFactory.createContent(myToolWindow.getContent(), "".false); toolWindow.getContentManager().addContent(content); }}Copy the code
Using Tool Windows in plug-ins comes in two forms:
- Declarative setup
plugin.xml
File registration, always visible to the user at any time available. - Programmatic Setup: Dynamic injection through the API interface, which can appear and hide before and after some operations.
Declarative Setup
<extensions defaultExtensionNs="com.intellij">
<! -- Add your extensions here -->
<toolWindow id="MyToolWindow" secondary="true" anchor="right" factoryClass="com.volcengine.plugin.toolwindow.MyToolWindowFactory"/>
</extensions>
Copy the code
Programmatic Setup
updateBaselineBtn.addActionListener(e -> {
BaselineWindow baselineWindow = new BaselineWindow(versionsJson, project, toolWindow);
ContentFactory contentFactory = ContentFactory.SERVICE.getInstance();
Content content = contentFactory.createContent(baselineWindow.getContent(), "".false);
toolWindow.getContentManager().addContent(content);
toolWindow.getContentManager().setSelectedContent(content);
});
Copy the code
UICreate a tool
Both the Wizard program and the Tool Window toolbar require the UI to fill the panel Content. Basically, all you need is a rich JPanel as Content. The UI in AS plug-in uses Java Swing components extensively, so those familiar with Swing will get started quickly. Here are some tips for generating UI in AS plug-in.
GUI Form
New –> Swing UI Designer –> GUI Form After filling in the information will generate a visual. Form file and bound Java classes, Add a getRootPanel method to the corresponding Java file to getRootPanel and use the built panel in the wizard or toolbar.
Eclipse – WindowBuilder
One drawback of the GUI forms mentioned above is that they only use Java, and the.fome file is strongly bound to the.java file. We can’t use the generated Java file alone, and GUI forms are awkward when we want to write pure Kotlin code.
Eclipse is a classic IDE that most students use when they are new to Java. There is a WindowBuilder plug-in that can also visually create GUI interfaces, but compared with GUI forms, WindowBuilder generates separate. Java files. The source code is generated for each step of the user’s operation in the GUI visual interface. We can copy this code directly into the AS plug-in and use the “Convert Java Code to Kotlin” function to convert this code into Kotlin code in one click, which is very convenient (more importantly, The WindowBuilder experience is personally better).
Kotlin UI DSL
IntelliJ based on Kotlin plugin official offer some domain specific language, can be written in the Kotlin code UI, advantage is that the code is exquisite, the disadvantage is that tired, specific refer to guide plugins.jetbrains.com/docs/intell website…
Data persistence
Sometimes we want to be able to save the user’s actions or configurations in the plug-in to avoid reworking the work and reading the necessary data, or to avoid the user typing more than once. The IntelliJ Platform provides some convenient apis for data persistence.
Plugin Service
These are the basic capabilities of IntelliJ plug-in development, which fall into three different types of services that can be used when we want to do some state and logical processing in the different life cycles of IDE plug-ins, for example: Persistent state, subscription events, Application startup/shutdown, Project startup/shutdown.
Service Interface type | Function description |
---|---|
Application Level | IDEA is initialized when it is started. Only one instance of IDEA exists in the lifecycle |
Project Level | IDEA creates a project-level instance for each Project instance |
Module Level | IDEA will load Module instances at the Module level for each Project, which may cause memory leaks in multi-module projects |
This code emulates the ability to automatically check if a new version exists when the IDE starts, and to update it if there is a new version, using persistent storage.
@State(name = "DemoConfiguration", storages = [
Storage(value = "demoConfiguration.xml")
])
class DemoComoponent:ApplicationComponent.PersistentStateComponent<DemoComoponent>, Serializable {
var version = 1
var localVersion = 0;
private fun isANerVersion(a) = localVersion < version
private fun updateVersion(a){
localVersion = version
}
override fun initComponent(a) {
if(isANerVersion()){
updateVersion()
}
}
override fun getState(a): DemoComoponent? = this
override fun loadState(state: DemoComoponent) {
XmlSerializerUtil.copyBean(state, this)}}Copy the code
There are two ways to persist storage
1. PropertiesComponent
This is a simple key-value data structure that can be used as a Map to hold application and project-level data.
// Get the Application-level PropertiesComponent
PropertiesComponent propertiesComponent = PropertiesComponent.getInstance();
// Get the Project-level PropertiesComponent and specify the corresponding project
PropertiesComponent propertiesComponent = PropertiesComponent.getInstance(Project);
// set & get
propertiesComponent.setValue(name, value)
propertiesComponent.getValue(name)
Copy the code
2. PersistentStateComponent
A complex type of data structure uses the PersistentStateComponent to specify where persistence is stored.
public interface PersistentStateComponent<T> {
@Nullable
T getState(a);
void loadState(T state);
}
Copy the code
- Create an implementation class that PersistentStateComponent, where T represents the type of data structure that needs to be persisted, which can be any class or even the implementation class itself, and override the getState and loadState methods.
- To specify where to store, add the * @state * annotation to the presentation class.
- If you do not want a field to be persisted, you can add a * @TRANSIENT * annotation to the field.
@State(
name = "ChentaoPlugin" ,
storages = [Storage("chentao-plugin.xml")])class AarCheckBoxSettings :PersistentStateComponent<HashMap<String, AarCheckBoxState>> {
var checkBoxStateList = HashMap<String, AarCheckBoxState>()
override fun getState(a): HashMap<String, AarCheckBoxState>? {
return checkBoxStateList
}
override fun loadState(stateList: HashMap<String, AarCheckBoxState>) {
checkBoxStateList = stateList
}
// The way to declare persistent components as Serveice is through the ServiceManager
companion object{
@JvmStatic
fun getInstance(a): PersistentStateComponent<HashMap<String, AarCheckBoxState>>{
return ServiceManager.getService(AarCheckBoxSettings::class.java)
}
}
}
data class AarCheckBoxState(val componentId:String, val isSelected:Boolean)
Copy the code
Register persistent components
The PersistentStateComponent implementation class needs to be registered as a Service in plugin.xml.
<extensions defaultExtensionNs="com.intellij">
<applicationService serviceImplementation="com.volcengine.plugin.actions.AarCheckBoxSettings"/>
</extensions>
Copy the code
Plug-in packaging and installation
- Package: Run the Assemble task in the Gradle toolbar to find the packaged plug-in ZIP package in the /build/distribution/{plug-in name}-{plug-in version}.zip path.
- Local installation: not yet will be published to the Plugin’s market before we can choose to Install local plug-in, open AS menu bar/Android Studio/Preference/Plugins/Install the Plugin from Disk… It can be used after installation.
- Release plugin market:
- Visit hub.jetbrains.com/ to create an account.
- Log in to jetBrains Marketplace plugins.jetbrains.com/ and publish plugins.
- The first version of the plugin requires manual upload on the website, and later versions can be automatically updated using the tokens in the Hub account.
conclusion
Review the development process: Core steps of the IDE plug-in: Install the correct version of IntelliJ –> Configuration Project –> Create Action –> Inject the complex process into the Wizard Wizard or ToolWindow toolbar (while creating the UI) –> Use Data Persistence to save the necessary data –> Package & Install & Publish.
I think the difficulty of IDE plug-in development is mainly the process of grope, IDE plug-in is not popular, there are few online introduction articles, the official website introduced some functions and components without detailed API guidance, it is a little difficult to start. Finally, by decompilating the source code of some official plug-ins (Firebase, Flutter, etc.) and collecting information from Google, Youtube and other blogs, the prototype of AS plug-in was built. Some common capabilities learned from Flutter were also sorted out in this article. Hope to help students who want to contact AS plug-in development later.
Demo
Github.com/ChentaoZhou…
About the Byte Terminal technology team
Bytedance Client Infrastructure is a global r&d team of big front-end Infrastructure technology (with r&d teams in Beijing, Shanghai, Hangzhou, Shenzhen, Guangzhou, Singapore and Mountain View), responsible for the construction of the whole big front-end Infrastructure of Bytedance. Improve the performance, stability and engineering efficiency of the company’s entire product line; The supported products include but are not limited to Douyin, Toutiao, Watermelon Video, Feishu, Kechedi, etc. We have in-depth research on mobile terminal, Web, Desktop and other terminals.
Now! Client/front-end/server/side intelligent algorithm/test development for global recruitment! Let’s change the world with technology. If you are interested, please contact [[email protected]](mailto:[email protected]) with email subject: Resume – Name – Job Objective – Desired city – Tel.
Bytes to beat application suite MARS is byte to beat terminal technology team in trill, today’s headlines over the past nine years, watermelon video, books, understand car such as emperor App development practice, for mobile research and development, the front-end development, QA, operations, product managers, project managers and operating roles, one-stop research and development of the overall solution, We will help enterprises upgrade their R&D models and reduce their overall r&d costs.