Skip to content

Commit

Permalink
Formatting based on external formatter (#80)
Browse files Browse the repository at this point in the history
Co-authored-by: Johannes Spangenberg <[email protected]>
  • Loading branch information
cottand and JojOatXGME authored Jun 8, 2024
1 parent b25d38d commit 0a5b4e4
Show file tree
Hide file tree
Showing 11 changed files with 363 additions and 24 deletions.
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());
}
}
});
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

0 comments on commit 0a5b4e4

Please sign in to comment.