diff --git a/src/main/java/org/nixos/idea/format/NixExternalFormatter.java b/src/main/java/org/nixos/idea/format/NixExternalFormatter.java new file mode 100644 index 00000000..a895d84a --- /dev/null +++ b/src/main/java/org/nixos/idea/format/NixExternalFormatter.java @@ -0,0 +1,107 @@ +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.settings.NixLangSettings; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.EnumSet; +import java.util.List; +import java.util.Scanner; +import java.util.Set; + +public final class NixExternalFormatter extends AsyncDocumentFormattingService { + + @Override + protected @NotNull String getNotificationGroupId() { + return "NixIDEA"; + } + + @Override + protected @NotNull @NlsSafe String getName() { + return "NixIDEA"; + } + + @Override + public @NotNull Set 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) { + NixLangSettings nixSettings = NixLangSettings.getInstance(); + System.out.println("started fmt task"); + if (!nixSettings.isFormatEnabled()) { + return null; + } + + var ioFile = request.getIOFile(); + if (ioFile == null) return null; + + @NonNls + var command = nixSettings.getFormatCommand(); + List argv = ParametersListUtil.parse(command, false, true); + + try { + var commandLine = new GeneralCommandLine(argv); + + OSProcessHandler handler = new OSProcessHandler(commandLine.withCharset(StandardCharsets.UTF_8)); + OutputStream processInput = handler.getProcessInput(); + Files.copy(ioFile.toPath(), processInput); + processInput.flush(); + processInput.close(); + 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(); + } + + @Override + public boolean cancel() { + handler.destroyProcess(); + return true; + } + + @Override + public boolean isRunUnderProgress() { + return true; + } + }; + } catch (ExecutionException | IOException e) { + request.onError("NixIDEA", e.getMessage()); + return null; + } + } +} diff --git a/src/main/java/org/nixos/idea/lsp/NixLspSettingsConfigurable.java b/src/main/java/org/nixos/idea/lsp/NixLspSettingsConfigurable.java index bdfcd081..3fe41802 100644 --- a/src/main/java/org/nixos/idea/lsp/NixLspSettingsConfigurable.java +++ b/src/main/java/org/nixos/idea/lsp/NixLspSettingsConfigurable.java @@ -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 BUILTIN_SUGGESTIONS = List.of( + CommandSuggestionsPopup.Suggestion.builtin("Use nil from nixpkgs", + "nix --extra-experimental-features \"nix-command flakes\" run nixpkgs#nil"), + CommandSuggestionsPopup.Suggestion.builtin("Use nixd from nixpkgs", + "nix --extra-experimental-features \"nix-command flakes\" run nixpkgs#nixd") + ); private @Nullable JBCheckBox myEnabled; private @Nullable RawCommandLineEditor myCommand; @@ -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) diff --git a/src/main/java/org/nixos/idea/lsp/ui/CommandSuggestionsPopup.java b/src/main/java/org/nixos/idea/lsp/ui/CommandSuggestionsPopup.java index af935163..18d804c1 100644 --- a/src/main/java/org/nixos/idea/lsp/ui/CommandSuggestionsPopup.java +++ b/src/main/java/org/nixos/idea/lsp/ui/CommandSuggestionsPopup.java @@ -37,20 +37,19 @@ import java.util.stream.Stream; public final class CommandSuggestionsPopup { - // Implementation partially inspired by TextCompletionField - private static final List BUILDIN_SUGGESTIONS = List.of( - Suggestion.builtin("Use nil from nixpkgs", - "nix --extra-experimental-features \"nix-command flakes\" run nixpkgs#nil"), - Suggestion.builtin("Use nixd from nixpkgs", - "nix --extra-experimental-features \"nix-command flakes\" run nixpkgs#nixd") - ); + // Implementation partially inspired by TextCompletionField private final @NotNull ExpandableTextField myEditor; private final @NotNull Collection myHistory; private @Nullable ListPopup myPopup; + private final @NotNull List mySuggestions; - public CommandSuggestionsPopup(@NotNull RawCommandLineEditor commandLineEditor, @NotNull Collection history) { + public CommandSuggestionsPopup(@NotNull RawCommandLineEditor commandLineEditor, + @NotNull Collection history, + @NotNull List suggestions + ) { + mySuggestions = suggestions; myEditor = commandLineEditor.getEditorField(); myHistory = history; } @@ -72,7 +71,7 @@ public void install() { public void show() { if (myPopup == null) { - myPopup = new MyListPopup(); + myPopup = new MyListPopup(mySuggestions); myPopup.showUnderneathOf(myEditor); } } @@ -110,8 +109,8 @@ public void caretUpdate(CaretEvent e) { } private final class MyListPopup extends ListPopupImpl implements JBPopupListener { - private MyListPopup() { - super(null, new MyListPopupStep()); + private MyListPopup(List suggestions) { + super(null, new MyListPopupStep(suggestions)); // Disable focus in popup, so that the text field stays in focus. setRequestFocus(false); // Prevent the popup from overriding the paste-action. @@ -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); } } @@ -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); } @@ -161,9 +161,9 @@ public String toString() { private final class MyListPopupStep extends BaseListPopupStep implements ListPopupStepEx { - public MyListPopupStep() { + public MyListPopupStep(List suggestions) { super(null, Stream.concat( - BUILDIN_SUGGESTIONS.stream(), + suggestions.stream(), myHistory.stream().map(Suggestion::history) ).toList()); } diff --git a/src/main/java/org/nixos/idea/settings/NixLangSettings.java b/src/main/java/org/nixos/idea/settings/NixLangSettings.java new file mode 100644 index 00000000..0ae034c0 --- /dev/null +++ b/src/main/java/org/nixos/idea/settings/NixLangSettings.java @@ -0,0 +1,78 @@ +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 = "NixLangSettings", storages = @Storage(value = "nix-idea-tools.xml", roamingType = RoamingType.DISABLED)) +public final class NixLangSettings implements PersistentStateComponent { + + // TODO: Use RoamingType.LOCAL with 2024.1 + + // 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 NixLangSettings getInstance() { + return ApplicationManager.getApplication().getService(NixLangSettings.class); + } + + public boolean isFormatEnabled() { + return myState.formatEnabled; + } + + public void setFormatEnabled(boolean enabled) { + myState.formatEnabled = enabled; + } + + public @NotNull String getFormatCommand() { + return myState.formatCommand; + } + + public void setFormatCommand(@NotNull String command) { + myState.formatCommand = command; + addFormatCommandToHistory(command); + } + + public @NotNull Collection getCommandHistory() { + return Collections.unmodifiableCollection(myState.formatCommandHistory); + } + + private void addFormatCommandToHistory(@NotNull String command) { + Deque history = myState.formatCommandHistory; + 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 formatEnabled = false; + public @NotNull String formatCommand = ""; + public Deque formatCommandHistory = new ArrayDeque<>(); + } +} diff --git a/src/main/java/org/nixos/idea/settings/NixLangSettingsConfigurable.java b/src/main/java/org/nixos/idea/settings/NixLangSettingsConfigurable.java new file mode 100644 index 00000000..1f45a9f0 --- /dev/null +++ b/src/main/java/org/nixos/idea/settings/NixLangSettingsConfigurable.java @@ -0,0 +1,110 @@ +package org.nixos.idea.settings; + +import com.intellij.openapi.options.Configurable; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.options.SearchableConfigurable; +import com.intellij.openapi.project.ProjectManager; +import com.intellij.openapi.util.NlsContexts; +import com.intellij.ui.RawCommandLineEditor; +import com.intellij.ui.TitledSeparator; +import com.intellij.ui.components.JBCheckBox; +import com.intellij.ui.components.JBLabel; +import com.intellij.ui.components.JBTextArea; +import com.intellij.util.ui.FormBuilder; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.nixos.idea.lsp.ui.CommandSuggestionsPopup; + +import javax.swing.*; +import java.util.List; + +public class NixLangSettingsConfigurable implements SearchableConfigurable, Configurable.Beta { + private static final List BUILTIN_SUGGESTIONS = List.of( + CommandSuggestionsPopup.Suggestion.builtin("Use nixpkgs-fmt from nixpkgs", + "nix --extra-experimental-features \"nix-command flakes\" run nixpkgs#nixpkgs-fmt") + ); + + private @Nullable JBCheckBox myEnabled; + private @Nullable RawCommandLineEditor myCommand; + private @Nullable JBLabel myTextArea; + + @Override + public @NotNull @NonNls String getId() { + return "org.nixos.idea.lsp.NixLangSettingsConfigurable"; + } + + @Override + public @NlsContexts.ConfigurableName String getDisplayName() { + return "Nix Language Server (LSP)"; + } + + @Override + public @Nullable JComponent createComponent() { + myEnabled = new JBCheckBox("Enable external formatter"); + myEnabled.addChangeListener(e -> updateUiState()); + + myCommand = new RawCommandLineEditor(); + myCommand.getEditorField().getEmptyText().setText("Command to execute for formatting"); + myCommand.getEditorField().getAccessibleContext().setAccessibleName("Command to execute for formatting"); + + myTextArea = new JBLabel(); + + myTextArea.setText("Format Nix files via an external formatter. Source of focused file will be passed as standard input."); + new CommandSuggestionsPopup( + myCommand, + NixLangSettings.getInstance().getCommandHistory(), + BUILTIN_SUGGESTIONS + ).install(); + + + + return FormBuilder.createFormBuilder() + .addComponent(new TitledSeparator("Formatter Configuration")) + .addComponent(myTextArea) + .addComponent(myEnabled) + .addLabeledComponent("Command: ", myCommand) + .addComponentFillVertically(new JPanel(), 0) + .getPanel(); + } + + @Override + public void reset() { + assert myEnabled != null; + assert myCommand != null; + + NixLangSettings settings = NixLangSettings.getInstance(); + myEnabled.setSelected(settings.isFormatEnabled()); + myCommand.setText(settings.getFormatCommand()); + + updateUiState(); + } + + @SuppressWarnings("UnstableApiUsage") + @Override + public void apply() throws ConfigurationException { + assert myEnabled != null; + assert myCommand != null; + + NixLangSettings settings = NixLangSettings.getInstance(); + settings.setFormatEnabled(myEnabled.isSelected()); + settings.setFormatCommand(myCommand.getText()); + } + + @Override + public boolean isModified() { + assert myEnabled != null; + assert myCommand != null; + + NixLangSettings settings = NixLangSettings.getInstance(); + return Configurable.isCheckboxModified(myEnabled, settings.isFormatEnabled()) || + Configurable.isFieldModified(myCommand.getTextField(), settings.getFormatCommand()); + } + + private void updateUiState() { + assert myEnabled != null; + assert myCommand != null; + + myCommand.setEnabled(myEnabled.isSelected()); + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index b247f2c9..3ea7e292 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -15,25 +15,25 @@ implementationClass="org.nixos.idea.file.NixFileType" fieldName="INSTANCE" language="Nix" - extensions="nix" /> + extensions="nix"/> + implementationClass="org.nixos.idea.lang.NixParserDefinition"/> + implementationClass="org.nixos.idea.lang.highlighter.NixSyntaxHighlighterFactory"/> - - + + + implementationClass="org.nixos.idea.lang.NixBraceMatcher"/> + instance="org.nixos.idea.settings.NixIDEASettings"/> + implementation="org.nixos.idea.settings.NixColorSettingsPage"/> + + + + +