Search code examples
intellij-ideaintellij-plugin

How to persist/read-back Run Configuration parameters in Intellij plugin


I'm making a basic IntelliJ plugin that lets a user define Run Configuration (following the tutorial at [1]), and use said Run Configurations to execute the file open in the editor on a remote server.

My Run Configuration is simple (3 text fields), and I have it all working, however, after editing the Run Configuration, and click "Apply" or "OK" after changing values, the entered values are lost.

What is the correct way to persist and read-back values (both when the Run Configuration is re-opened as well as when the Run Configuration's Runner invoked)? It looks like I could try to create a custom persistence using [2], however, it seems like the Plugin framework should have a way to handle this already or at least hooks for when Apply/OK is pressed.

[1] https://www.jetbrains.org/intellij/sdk/docs/tutorials/run_configurations.html

[2] https://www.jetbrains.org/intellij/sdk/docs/basics/persisting_state_of_components.html


Solution

  • Hopefully, this post is a bit more clear to those new to IntelliJ plugin development and illustrates how persisting/loading Run Configurations can be achieved. Please read through the code comments as this is where much of the explanation takes place.

    Also now that SettingsEditorImpl is my custom implementation of the SettingsEditor abstract class, and likewise, RunConfigurationImpl is my custom implementation of the RunConfigiration abstract class.

    The first thing to do is to expose the form fields via custom getters on your SettingsEditorImpl (ie. getHost())

    public class SettingsEditorImpl extends SettingsEditor<RunConfigurationImpl> {
        private JPanel configurationPanel; // This is the outer-most JPanel
        private JTextField hostJTextField;
    
        public SettingsEditorImpl() {
            super();
        }
    
        @NotNull
        @Override
        protected JComponent createEditor() {
            return configurationPanel;
        }
    
        /* Gets the Form fields value */
        private String getHost() {
            return hostJTextField.getText();
        }
    
        /* Copy value FROM your custom runConfiguration back INTO the Form UI; This is to load previously saved values into the Form when it's opened. */
        @Override
        protected void resetEditorFrom(RunConfigurationImpl runConfiguration) {
            hostJTextField.setText(StringUtils.defaultIfBlank(runConfiguration.getHost(), RUN_CONFIGURATION_HOST_DEFAULT));
        }
    
        /* Sync the value from the Form UI INTO the RunConfiguration which is what the rest of your code will interact with. This requires a way to set this value on your custom RunConfiguration, ie. RunConfigurationImpl@#setHost(host)  */
        @Override
        protected void applyEditorTo(RunConfigurationImpl runConfiguration) throws ConfigurationException {
            runConfiguration.setHost(getHost());
        }
    }
    

    So now, the custom SettingsEditor, which backs the Form UI, is set up to Sync field values In and Out of itself. Remember, the custom RunConfiguration is what is going to actually represent this configuration; the SettingsEditor implementation just represents the FORM (a subtle difference, but important).

    Now we need a custom RunConfiguration ...

    /* Annotate the class with @State and @Storage, which is used to define how this RunConfiguration's data will be persisted/loaded. */
    @State(
            name = Constants.PLUGIN_NAME,
            storages = {@Storage(Constants.PLUGIN_NAME + "__run-configuration.xml")}
    )
    public class RunConfigurationImpl extends RunConfigurationBase {
         // Its good to 'namespace' keys to your component;
        public static final String KEY_HOST = Constants.PLUGIN_NAME + ".host";
    
    
        private String host;
    
        public RunConfigurationImpl(Project project, ConfigurationFactory factory, String name) {
            super(project, factory, name);
        }
    
        /* Return an instances of the custom SettingsEditor ... see class defined above */
        @NotNull
        @Override
        public SettingsEditor<? extends RunConfiguration> getConfigurationEditor() {
            return new SettingsEditorImpl();
        }
    
        /* Return null, else we'll get a Startup/Connection tab in our Run Configuration UI in IntelliJ */
        @Nullable
        @Override
        public SettingsEditor<ConfigurationPerRunnerSettings> getRunnerSettingsEditor(ProgramRunner runner) {
            return null;
        }
    
        /* This is a pretty cool method. Every time SettingsEditor#applyEditorTo() is changed the values in this class, this method is run and can check/validate any fields! If RuntimeConfigurationException is thrown, the exceptions message is shown at the bottom of the Run Configuration UI in IntelliJ! */
        @Override
        public void checkConfiguration() throws RuntimeConfigurationException {
            if (!StringUtils.startsWithAny(getHost(), "http://", "https://")) {
                throw new RuntimeConfigurationException("Invalid host");
            }
        }
    
        @Nullable
        @Override
        public RunProfileState getState(@NotNull Executor executor, @NotNull ExecutionEnvironment executionEnvironment) throws ExecutionException {
            return null;
        }
    
        /* This READS any prior persisted configuration from the State/Storage defined by this classes annotations ... see above.
        You must manually read and populate the fields using JDOMExternalizerUtil.readField(..).
        This method is invoked at the "right time" by the plugin framework. You dont need to call this.
        */
        @Override
        public void readExternal(Element element) throws InvalidDataException {
            super.readExternal(element);
            host = JDOMExternalizerUtil.readField(element, KEY_HOST);
        }
    
        /* This WRITES/persists configurations TO the State/Storage defined by this classes annotations ... see above.
        You must manually read and populate the fields using JDOMExternalizerUtil.writeField(..).
        This method is invoked at the "right time" by the plugin framework. You dont need to call this.
        */
        @Override
        public void writeExternal(Element element) throws WriteExternalException {
            super.writeExternal(element);
            JDOMExternalizerUtil.writeField(element, KEY_HOST, host);
    
        }
    
        /* This method is what's used by the rest of the plugin code to access the configured 'host' value. The host field (variable) is written by
        1. when writeExternal(..) loads a value from a persisted config.
        2. when SettingsEditor#applyEditorTo(..) is called when the Form itself changes.
        */
        public String getHost() {
            return host;
        }
    
        /* This method sets the value, and is primarily used by the custom SettingEditor's SettingsEditor#applyEditorTo(..) method call */
        public void setHost(String host) {
            this.host = host;
        }
    

    }

    To read these configuration values elsewhere, say for example a custom ProgramRunner, you would do something like:

    final RunConfigurationImpl runConfiguration = (RunConfigurationImpl) executionEnvironment.getRunnerAndConfigurationSettings().getConfiguration();
    runConfiguration.getHost(); // Returns the configured host value