Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Formatting based on external formatter #80

Merged
merged 11 commits into from
Jun 8, 2024
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ hs_err_pid*
*.iml
**/.gradle
build

src/gen
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Added

- Support for code formatting via external commands ([#80](https://github.com/NixOS/nix-idea/pull/80))

### Changed

### Deprecated
Expand Down
112 changes: 112 additions & 0 deletions src/main/java/org/nixos/idea/format/NixExternalFormatter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package org.nixos.idea.format;

import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.process.CapturingProcessAdapter;
import com.intellij.execution.process.OSProcessHandler;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.formatting.service.AsyncDocumentFormattingService;
import com.intellij.formatting.service.AsyncFormattingRequest;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.psi.PsiFile;
import com.intellij.util.execution.ParametersListUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.nixos.idea.file.NixFile;
import org.nixos.idea.lang.NixLanguage;
import org.nixos.idea.settings.NixExternalFormatterSettings;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;

public final class NixExternalFormatter extends AsyncDocumentFormattingService {

@Override
protected @NotNull String getNotificationGroupId() {
return NixLanguage.NOTIFICATION_GROUP_ID;
}

@Override
protected @NotNull @NlsSafe String getName() {
return "NixIDEA";
}

@Override
public @NotNull Set<Feature> getFeatures() {
return EnumSet.noneOf(Feature.class);
}

@Override
public boolean canFormat(@NotNull PsiFile psiFile) {
return psiFile instanceof NixFile;
}


@Override
protected @Nullable FormattingTask createFormattingTask(@NotNull AsyncFormattingRequest request) {
NixExternalFormatterSettings nixSettings = NixExternalFormatterSettings.getInstance();
if (!nixSettings.isFormatEnabled()) {
return null;
}

var ioFile = request.getIOFile();
if (ioFile == null) return null;

@NonNls
var command = nixSettings.getFormatCommand();
List<String> argv = ParametersListUtil.parse(command, false, true);

var commandLine = new GeneralCommandLine(argv);

try {
var handler = new OSProcessHandler(commandLine.withCharset(StandardCharsets.UTF_8));
OutputStream processInput = handler.getProcessInput();
return new FormattingTask() {
@Override
public void run() {
handler.addProcessListener(new CapturingProcessAdapter() {

@Override
public void processTerminated(@NotNull ProcessEvent event) {
int exitCode = event.getExitCode();
if (exitCode == 0) {
request.onTextReady(getOutput().getStdout());
} else {
request.onError("NixIDEA", getOutput().getStderr());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (non-blocking): Should we maybe use “Nix External Formatter” for the title? (Same in catch block.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is clearer to use the plugin name as title, so it is clear who is 'to blame' for the error, but I do not have a strong opinion.

I can change it to Nix External Formatter if you prefer

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am fine with both. I think both have their pros and cons. 😅

}
}
});
handler.startNotify();
try {
Files.copy(ioFile.toPath(), processInput);
processInput.flush();
processInput.close();
} catch (IOException e) {
handler.destroyProcess();
request.onError("NixIDEA", e.getMessage());
}
}

@Override
public boolean cancel() {
handler.destroyProcess();
return true;
}

@Override
public boolean isRunUnderProgress() {
return true;
}
};
} catch (ExecutionException e) {
request.onError("NixIDEA", e.getMessage());
return null;
}
}
}
1 change: 1 addition & 0 deletions src/main/java/org/nixos/idea/lang/NixLanguage.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

public class NixLanguage extends Language {
public static final NixLanguage INSTANCE = new NixLanguage();
public static final String NOTIFICATION_GROUP_ID = "NixIDEA";

private NixLanguage() {
super("Nix");
Expand Down
5 changes: 2 additions & 3 deletions src/main/java/org/nixos/idea/lsp/NixLspSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,16 @@
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import org.jetbrains.annotations.NotNull;
import org.nixos.idea.settings.NixStoragePaths;

import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;

@State(name = "NixLspSettings", storages = @Storage(value = "nix-idea-tools.xml", roamingType = RoamingType.DISABLED))
@State(name = "NixLspSettings", storages = @Storage(value = NixStoragePaths.TOOLS, roamingType = RoamingType.DISABLED))
public final class NixLspSettings implements PersistentStateComponent<NixLspSettings.State> {

// TODO: Use RoamingType.LOCAL with 2024.1

// Documentation:
// https://plugins.jetbrains.com/docs/intellij/persisting-state-of-components.html

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,15 @@

import javax.swing.JComponent;
import javax.swing.JPanel;
import java.util.List;

public class NixLspSettingsConfigurable implements SearchableConfigurable, Configurable.Beta {
private static final List<CommandSuggestionsPopup.Suggestion> BUILTIN_SUGGESTIONS = List.of(
CommandSuggestionsPopup.Suggestion.builtin("<html>Use <b>nil</b> from nixpkgs</html>",
"nix --extra-experimental-features \"nix-command flakes\" run nixpkgs#nil"),
CommandSuggestionsPopup.Suggestion.builtin("<html>Use <b>nixd</b> from nixpkgs</html>",
"nix --extra-experimental-features \"nix-command flakes\" run nixpkgs#nixd")
);

private @Nullable JBCheckBox myEnabled;
private @Nullable RawCommandLineEditor myCommand;
Expand All @@ -43,7 +50,7 @@ public class NixLspSettingsConfigurable implements SearchableConfigurable, Confi
myCommand.getEditorField().getEmptyText().setText("Command to start Language Server");
myCommand.getEditorField().getAccessibleContext().setAccessibleName("Command to start Language Server");
myCommand.getEditorField().setMargin(myEnabled.getMargin());
new CommandSuggestionsPopup(myCommand, NixLspSettings.getInstance().getCommandHistory()).install();
new CommandSuggestionsPopup(myCommand, NixLspSettings.getInstance().getCommandHistory(), BUILTIN_SUGGESTIONS).install();

return FormBuilder.createFormBuilder()
.addComponent(myEnabled)
Expand Down
24 changes: 12 additions & 12 deletions src/main/java/org/nixos/idea/lsp/ui/CommandSuggestionsPopup.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,19 @@
import java.util.stream.Stream;

public final class CommandSuggestionsPopup {
// Implementation partially inspired by TextCompletionField

private static final List<Suggestion> BUILDIN_SUGGESTIONS = List.of(
Suggestion.builtin("<html>Use <b>nil</b> from nixpkgs</html>",
"nix --extra-experimental-features \"nix-command flakes\" run nixpkgs#nil"),
Suggestion.builtin("<html>Use <b>nixd</b> from nixpkgs</html>",
"nix --extra-experimental-features \"nix-command flakes\" run nixpkgs#nixd")
);
// Implementation partially inspired by TextCompletionField

private final @NotNull ExpandableTextField myEditor;
private final @NotNull Collection<String> myHistory;
private @Nullable ListPopup myPopup;
private final @NotNull List<Suggestion> mySuggestions;

public CommandSuggestionsPopup(@NotNull RawCommandLineEditor commandLineEditor, @NotNull Collection<String> history) {
public CommandSuggestionsPopup(@NotNull RawCommandLineEditor commandLineEditor,
@NotNull Collection<String> history,
@NotNull List<Suggestion> suggestions
) {
mySuggestions = suggestions;
myEditor = commandLineEditor.getEditorField();
myHistory = history;
}
Expand Down Expand Up @@ -127,7 +126,8 @@ protected void process(KeyEvent aEvent) {
switch (aEvent.getKeyCode()) {
// Do no handle left and right key,
// as it would prevent their usage in the text field while the popup is open.
case KeyEvent.VK_LEFT, KeyEvent.VK_RIGHT -> {}
case KeyEvent.VK_LEFT, KeyEvent.VK_RIGHT -> {
}
default -> super.process(aEvent);
}
}
Expand All @@ -138,13 +138,13 @@ public void onClosed(@NotNull LightweightWindowEvent event) {
}
}

private record Suggestion(
public record Suggestion(
@NotNull Icon icon,
@NotNull String primaryText,
@Nullable String secondaryText,
@NotNull String command
) {
static @NotNull Suggestion builtin(@NotNull String name, @NotNull String command) {
public static @NotNull Suggestion builtin(@NotNull String name, @NotNull String command) {
return new Suggestion(AllIcons.Actions.Lightning, name, command, command);
}

Expand All @@ -163,7 +163,7 @@ private final class MyListPopupStep extends BaseListPopupStep<Suggestion> implem

public MyListPopupStep() {
super(null, Stream.concat(
BUILDIN_SUGGESTIONS.stream(),
mySuggestions.stream(),
myHistory.stream().map(Suggestion::history)
).toList());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.nixos.idea.settings;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.RoamingType;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import org.jetbrains.annotations.NotNull;

import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;

@State(name = "NixExternalFormatterSettings", storages = @Storage(value = NixStoragePaths.TOOLS, roamingType = RoamingType.DISABLED))
public final class NixExternalFormatterSettings implements PersistentStateComponent<NixExternalFormatterSettings.State> {

// Documentation:
// https://plugins.jetbrains.com/docs/intellij/persisting-state-of-components.html

private static final int MAX_HISTORY_SIZE = 5;

private @NotNull State myState = new State();

public static @NotNull NixExternalFormatterSettings getInstance() {
return ApplicationManager.getApplication().getService(NixExternalFormatterSettings.class);
}

public boolean isFormatEnabled() {
return myState.enabled;
}

public void setFormatEnabled(boolean enabled) {
myState.enabled = enabled;
}

public @NotNull String getFormatCommand() {
return myState.command;
}

public void setFormatCommand(@NotNull String command) {
myState.command = command;
addFormatCommandToHistory(command);
}

public @NotNull Collection<String> getCommandHistory() {
return Collections.unmodifiableCollection(myState.history);
}

private void addFormatCommandToHistory(@NotNull String command) {
Deque<String> history = myState.history;
history.remove(command);
history.addFirst(command);
while (history.size() > MAX_HISTORY_SIZE) {
history.removeLast();
}
}

@SuppressWarnings("ClassEscapesDefinedScope")
@Override
public void loadState(@NotNull State state) {
myState = state;
}

@SuppressWarnings("ClassEscapesDefinedScope")
@Override
public @NotNull State getState() {
return myState;
}

static final class State {
public boolean enabled = false;
public @NotNull String command = "";
public Deque<String> history = new ArrayDeque<>();
}
}
Loading