Skip to content

Commit 6c004d0

Browse files
committed
Add the native binary to the IDEA plugin's settings page
Use native binary switches the native formatter on. Under it, while it is ticked, a line says which embedded binary there is: Use embedded version 21.0.1, with Update to 21.0.2 once the plugin is newer; No embedded version, with Download; the download under way, or why it failed, with Retry. The line follows the download while the page is open. Below it, Binary takes a binary of the user's own, with the usual browse button, and empty means the embedded one. Apply refuses the native formatter with nothing to run, and a path that is not an executable file. Formatter type now follows the embedded binary too, since it can arrive while the page is open. The page takes its EmbeddedNativeImage from the constructor, so the tests give it one over a temporary directory, and statusLine() is a plain function of the status: every state is checked without the version of the plugin, which in CI is a commit hash.
1 parent 9c65b83 commit 6c004d0

2 files changed

Lines changed: 292 additions & 20 deletions

File tree

‎open-java-format-idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirJavaFormatConfigurable.java‎

Lines changed: 141 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,28 @@
1616

1717
package com.palantir.javaformat.intellij;
1818

19+
import com.intellij.openapi.Disposable;
20+
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
1921
import com.intellij.openapi.options.BaseConfigurable;
2022
import com.intellij.openapi.options.ConfigurationException;
2123
import com.intellij.openapi.options.SearchableConfigurable;
2224
import com.intellij.openapi.project.Project;
25+
import com.intellij.openapi.ui.TextBrowseFolderListener;
26+
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
27+
import com.intellij.openapi.util.Disposer;
28+
import com.intellij.ui.components.ActionLink;
29+
import com.intellij.ui.components.JBTextField;
2330
import com.intellij.util.ui.FormBuilder;
31+
import com.intellij.util.ui.JBUI;
32+
import com.palantir.javaformat.intellij.EmbeddedNativeImage.Status;
2433
import com.palantir.javaformat.intellij.PalantirJavaFormatSettings.EnabledState;
34+
import java.awt.FlowLayout;
35+
import java.nio.file.Files;
36+
import java.nio.file.InvalidPathException;
37+
import java.nio.file.Path;
38+
import java.util.Optional;
2539
import javax.annotation.Nullable;
40+
import javax.swing.Box;
2641
import javax.swing.JCheckBox;
2742
import javax.swing.JComponent;
2843
import javax.swing.JLabel;
@@ -33,24 +48,61 @@
3348
class PalantirJavaFormatConfigurable extends BaseConfigurable implements SearchableConfigurable {
3449

3550
private final Project project;
51+
private final EmbeddedNativeImage embeddedNativeImage;
3652
private final JPanel panel;
3753
private final JCheckBox enable;
54+
private final JCheckBox useNativeImage;
55+
private final JPanel nativeImageOptions;
56+
private final JLabel embeddedStatus;
57+
private final ActionLink embeddedAction;
58+
private final TextFieldWithBrowseButton nativeImagePath;
3859
private final JLabel formatterType;
3960
private final JLabel formatterVersion;
4061
private final JLabel pluginVersion;
4162

63+
// Lives from createComponent() to disposeUIResources(): the download reports its progress to the page.
64+
@Nullable
65+
private Disposable uiDisposable;
66+
4267
public PalantirJavaFormatConfigurable(Project project) {
68+
this(project, EmbeddedNativeImage.getInstance());
69+
}
70+
71+
PalantirJavaFormatConfigurable(Project project, EmbeddedNativeImage embeddedNativeImage) {
4372
this.project = project;
73+
this.embeddedNativeImage = embeddedNativeImage;
4474
this.enable = new JCheckBox("Enable open-java-format");
75+
this.useNativeImage = new JCheckBox("Use native binary");
76+
this.embeddedStatus = new JLabel();
77+
this.embeddedAction = new ActionLink();
78+
embeddedAction.addActionListener(event -> embeddedNativeImage.download(project, pluginVersion()));
79+
var pathField = new JBTextField();
80+
pathField.getEmptyText().setText("Leave empty to use the embedded version");
81+
this.nativeImagePath = new TextFieldWithBrowseButton(pathField);
82+
nativeImagePath.addBrowseFolderListener(new TextBrowseFolderListener(
83+
FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor().withTitle("Native Binary"), project));
4584
this.pluginVersion = new JLabel();
4685
this.formatterVersion = new JLabel();
4786
this.formatterType = new JLabel();
4887

88+
var statusRow = new JPanel(new FlowLayout(FlowLayout.LEADING, 0, 0));
89+
statusRow.add(embeddedStatus);
90+
statusRow.add(Box.createHorizontalStrut(JBUI.scale(6)));
91+
statusRow.add(embeddedAction);
92+
this.nativeImageOptions = FormBuilder.createFormBuilder()
93+
.setFormLeftIndent(JBUI.scale(20))
94+
.addComponent(statusRow)
95+
.addLabeledComponent("Binary", nativeImagePath)
96+
.getPanel();
97+
useNativeImage.addItemListener(event -> nativeImageOptions.setVisible(useNativeImage.isSelected()));
98+
4999
// Laid out here rather than by the IntelliJ GUI designer. A .form is woven into the class at build
50100
// time, and this build does not instrument bytecode (see build.gradle), which left every field
51101
// above null: the page rendered nothing and reset() threw the moment it was opened.
52102
this.panel = FormBuilder.createFormBuilder()
53103
.addComponent(enable)
104+
.addComponent(useNativeImage)
105+
.addComponent(nativeImageOptions)
54106
.addLabeledComponent("Plugin version", pluginVersion)
55107
.addLabeledComponent("Implementation version", formatterVersion)
56108
.addLabeledComponent("Formatter type", formatterType)
@@ -85,13 +137,29 @@ public String getHelpTopic() {
85137
@Nullable
86138
@Override
87139
public JComponent createComponent() {
140+
if (uiDisposable == null) {
141+
uiDisposable = Disposer.newDisposable("open-java-format settings page");
142+
embeddedNativeImage.addChangeListener(this::showEmbeddedNativeImage, uiDisposable);
143+
}
88144
return panel;
89145
}
90146

91147
@Override
92148
public void apply() throws ConfigurationException {
93149
PalantirJavaFormatSettings settings = PalantirJavaFormatSettings.getInstance(project);
150+
var path = enteredNativeImagePath();
151+
if (useNativeImage.isSelected()) {
152+
if (path.isPresent() && !(Files.isRegularFile(path.get()) && Files.isExecutable(path.get()))) {
153+
throw new ConfigurationException(path.get() + " is not an executable file");
154+
}
155+
if (path.isEmpty() && embeddedNativeImage.installed().isEmpty()) {
156+
throw new ConfigurationException("Download the embedded version or choose a binary");
157+
}
158+
}
94159
settings.setEnabled(enable.isSelected() ? EnabledState.ENABLED : getDisabledState());
160+
settings.setUseNativeImage(useNativeImage.isSelected());
161+
settings.setNativeImagePath(path);
162+
showEmbeddedNativeImage();
95163
}
96164

97165
private EnabledState getDisabledState() {
@@ -105,21 +173,89 @@ private EnabledState getDisabledState() {
105173
public void reset() {
106174
PalantirJavaFormatSettings settings = PalantirJavaFormatSettings.getInstance(project);
107175
enable.setSelected(settings.isEnabled());
176+
useNativeImage.setSelected(settings.useNativeImage());
177+
nativeImageOptions.setVisible(settings.useNativeImage());
178+
nativeImagePath.setText(
179+
settings.getNativeImagePath().map(Path::toString).orElse(""));
108180
pluginVersion.setText(settings.getImplementationVersion().orElse("unknown"));
109181
formatterVersion.setText(getFormatterVersionText(settings));
110-
formatterType.setText(FormatterSelection.of(
111-
settings, EmbeddedNativeImage.getInstance().installedPath())
112-
.describe());
182+
showEmbeddedNativeImage();
113183
}
114184

115185
@Override
116186
public boolean isModified() {
117187
PalantirJavaFormatSettings settings = PalantirJavaFormatSettings.getInstance(project);
118-
return enable.isSelected() != settings.isEnabled();
188+
Optional<Path> path;
189+
try {
190+
path = enteredNativeImagePath();
191+
} catch (ConfigurationException e) {
192+
return true;
193+
}
194+
return enable.isSelected() != settings.isEnabled()
195+
|| useNativeImage.isSelected() != settings.useNativeImage()
196+
|| !path.equals(settings.getNativeImagePath());
119197
}
120198

121199
@Override
122-
public void disposeUIResources() {}
200+
public void disposeUIResources() {
201+
if (uiDisposable != null) {
202+
Disposer.dispose(uiDisposable);
203+
uiDisposable = null;
204+
}
205+
}
206+
207+
record StatusLine(String text, Optional<String> action) {}
208+
209+
/** The line under Use native binary, and the link next to it, which downloads the plugin's version. */
210+
static StatusLine statusLine(Status status) {
211+
return switch (status) {
212+
case Status.NoImageForPlatform noImage ->
213+
new StatusLine(
214+
"No native binary is published for " + System.getProperty("os.name") + " "
215+
+ System.getProperty("os.arch") + ".",
216+
Optional.empty());
217+
case Status.NotDownloaded notDownloaded -> new StatusLine("No embedded version.", Optional.of("Download"));
218+
case Status.Downloading downloading ->
219+
new StatusLine("Downloading embedded version " + downloading.version() + "…", Optional.empty());
220+
case Status.DownloadFailed failed ->
221+
new StatusLine("Download failed: " + failed.reason() + ".", Optional.of("Retry"));
222+
case Status.Current current ->
223+
new StatusLine("Use embedded version " + current.installed().version(), Optional.empty());
224+
case Status.Outdated outdated ->
225+
new StatusLine(
226+
"Use embedded version " + outdated.installed().version() + ".",
227+
Optional.of("Update to " + outdated.available()));
228+
};
229+
}
230+
231+
private void showEmbeddedNativeImage() {
232+
var line = statusLine(embeddedNativeImage.status(pluginVersion()));
233+
embeddedStatus.setText(line.text());
234+
embeddedAction.setText(line.action().orElse(""));
235+
embeddedAction.setVisible(line.action().isPresent());
236+
// What runs with the settings as applied: the embedded binary can arrive while the page is open.
237+
formatterType.setText(FormatterSelection.of(
238+
PalantirJavaFormatSettings.getInstance(project), embeddedNativeImage.installedPath())
239+
.describe());
240+
}
241+
242+
private String pluginVersion() {
243+
return PalantirJavaFormatSettings.getInstance(project)
244+
.getImplementationVersion()
245+
.orElse("unknown");
246+
}
247+
248+
private Optional<Path> enteredNativeImagePath() throws ConfigurationException {
249+
var text = nativeImagePath.getText().strip();
250+
if (text.isEmpty()) {
251+
return Optional.empty();
252+
}
253+
try {
254+
return Optional.of(Path.of(text));
255+
} catch (InvalidPathException e) {
256+
throw new ConfigurationException(text + " is not a path: " + e.getReason());
257+
}
258+
}
123259

124260
private static String getFormatterVersionText(PalantirJavaFormatSettings settings) {
125261
String suffix = settings.injectedVersionIsOutdated() ? " (using bundled)" : " (using injected)";

0 commit comments

Comments
 (0)