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

Add 'Run spotless' to Save Actions #54

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,24 @@
pluginGroup = com.github.ragurney.spotless
pluginName = Spotless Gradle
# SemVer format -> https://semver.org
pluginVersion = 2.0.0
pluginVersion = 2.1.0

# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
# for insight into build numbers and IntelliJ Platform versions.
pluginSinceBuild = 211
pluginSinceBuild = 241
# Removed to ease compatibility of future IntelliJ releases and this plugin. See GH issue #43
#pluginUntilBuild = null

# IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties
platformType = IC
platformVersion = 2021.1
platformVersion = 2024.1.2

# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
platformPlugins = com.intellij.gradle

# Java language level used to compile sources and to generate the files for - Java 11 is required since 2020.3
javaVersion = 11
javaVersion = 17

# Gradle Releases -> https://github.com/gradle/gradle/releases
gradleVersion = 7.3
Expand Down
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
rootProject.name = "spotless"
rootProject.name = "spotless-intellij-gradle"
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.github.ragurney.spotless.actions;

import com.github.ragurney.spotless.config.SpotlessConfiguration;
import com.github.ragurney.spotless.config.SpotlessOnSaveOptions;
import com.intellij.ide.actionsOnSave.impl.ActionsOnSaveFileDocumentManagerListener;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/** Action to be called whenever a document is saved */
public class SpotlessActionOnSave extends ActionsOnSaveFileDocumentManagerListener.ActionOnSave {
public static boolean isActiveFile(
@NotNull PsiFile psiFile, @NotNull Project project) {
var virtualFile = psiFile.getVirtualFile();;
var fileEditorManager = FileEditorManager.getInstance(project);

return Optional.ofNullable(fileEditorManager.getSelectedTextEditor())
.map(Editor::getVirtualFile)
.filter(virtualFile::equals)
.isPresent();
}

private boolean isFileSelectedForFormatting(@NotNull PsiFile psiFile, @NotNull SpotlessOnSaveOptions options) {
return options.isFileTypeSelected(psiFile.getFileType());
}

private static @Nullable PsiFile getPsiFile(@NotNull Project project, Document editor) {
return PsiDocumentManager.getInstance(project).getPsiFile(editor);
}

@Override
public boolean isEnabledForProject(@NotNull Project project) {
return SpotlessConfiguration.getInstance(project).isRunOnSaveEnabled();
}

@Override
public void processDocuments(@NotNull Project project, @NotNull Document @NotNull [] documents) {
var options = SpotlessOnSaveOptions.getInstance(project);

if (!options.isRunOnSaveEnabled()) {
return;
}

Arrays.stream(documents)
.map(document -> getPsiFile(project, document))
.filter(Objects::nonNull)
.filter(file -> isActiveFile(file, project))
.filter(file -> isFileSelectedForFormatting(file, options))
.forEach(file -> new ReformatCodeProcessor(file).run());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.github.ragurney.spotless.config;

import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.NotNull;

/** Configuration to store whether spotless should be executed on save */
@State(name = "SpotlessConfiguration", storages = @Storage("spotless.xml"))
public class SpotlessConfiguration
implements PersistentStateComponent<SpotlessConfiguration.State> {

private static final boolean SPOTLESS_ON_SAVE_DEFAULT = false;
private @NotNull State state = new State();

@NotNull
public static SpotlessConfiguration getInstance(@NotNull Project project) {
return project.getService(SpotlessConfiguration.class);
}

@Override
public @NotNull State getState() {
return state;
}

@Override
public void loadState(@NotNull State state) {
this.state = state;
}

public boolean isRunOnSaveEnabled() {
return state.runOnSave;
}

public void setRunOnSave(boolean runOnSave) {
state.runOnSave = runOnSave;
}

static class State {
public boolean runOnSave = SPOTLESS_ON_SAVE_DEFAULT;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the
// Apache 2.0 license.
package com.github.ragurney.spotless.config;

import com.intellij.ide.actionsOnSave.ActionOnSaveContext;
import com.intellij.lang.Language;
import com.intellij.lang.LanguageFormatting;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypeManager;
import com.intellij.openapi.fileTypes.LanguageFileType;
import com.intellij.openapi.util.Key;
import com.intellij.psi.codeStyle.CodeStyleSettingsProvider;
import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider;
import com.intellij.ui.components.ActionLink;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class SpotlessOnSaveActionInfo extends SpotlessOnSaveActionInfoBase<SpotlessOnSaveOptions> {

private static final Key<SpotlessOnSaveOptions> CURRENT_UI_STATE_KEY =
Key.create("spotless.format.on.save.options");

public SpotlessOnSaveActionInfo(@NotNull ActionOnSaveContext context) {
super(context, "Run spotless", CURRENT_UI_STATE_KEY);
}

@Override
protected @NotNull SpotlessOnSaveOptions getOptionsFromStoredState() {
return SpotlessOnSaveOptions.getInstance(getProject());
}

@Override
public @NotNull List<? extends ActionLink> getActionLinks() {
return Collections.emptyList();
}

@Override
protected void addApplicableFileTypes(@NotNull Collection<? super FileType> result) {
// The formatting capability of a file is generally determined by its programming language
// rather than its file type.
// The UI task involves displaying all file types that
// - have the defined file name pattern (fileTypeManager.getAssociations())
// - can be formatted
// We perform various language-based checks to assess the code's formattability.
// Upon successful verification, we add all associated file types for that language to the UI
// list.

// Another proposed solution involves displaying only the file types associated
// with each language aka `language.getAssociatedFileType()`.
// However, this approach was rejected (lost significant JS dialects),
// and the details can be found in the file history (CPP-37117).

// prepare the language to "file types with patterns" map
MultiMap<Language, LanguageFileType> languageFileTypes = new MultiMap<>();
FileTypeManager fileTypeManager = FileTypeManager.getInstance();
for (FileType fileType : fileTypeManager.getRegisteredFileTypes()) {
if (fileType instanceof LanguageFileType lft
&& !fileTypeManager.getAssociations(fileType).isEmpty()) {
languageFileTypes.putValue(lft.getLanguage(), lft);
}
}

// if the language is formattable, add all file types from the created map
Consumer<Language> addLanguageFileTypes =
(@Nullable Language language) -> {
if (language != null) {
result.addAll(languageFileTypes.get(language));
ContainerUtil.addIfNotNull(result, language.getAssociatedFileType());
}
};

// add all file types that can be handled by the IDE internal formatter (== have
// FormattingModelBuilder)
for (Language language : languageFileTypes.keySet()) {
if (LanguageFormatting.INSTANCE.forLanguage(language) != null) {
addLanguageFileTypes.accept(language);
}
}

// Iterating only FormattingModelBuilders is not enough. Some file types may get formatted by
// external formatter integrated in the IDE (like ShExternalFormatter).
//
// A good sign that IDE supports some file type formatting is that it has a Code Style page for
// this file type. The following code makes
// sure that all file types that have their Code Style pages are included in the result set.
//
// The logic of iterating Code Style pages is similar to what's done in
// CodeStyleSchemesConfigurable.buildConfigurables()
for (CodeStyleSettingsProvider provider :
CodeStyleSettingsProvider.EXTENSION_POINT_NAME.getExtensionList()) {
if (provider.hasSettingsPage()) {
addLanguageFileTypes.accept(provider.getLanguage());
}
}
for (LanguageCodeStyleSettingsProvider provider :
LanguageCodeStyleSettingsProvider.getSettingsPagesProviders()) {
addLanguageFileTypes.accept(provider.getLanguage());
}
}

@Override
protected void apply() {
getOptionsFromStoredState().loadState(getCurrentUiState().getState().clone());
}
}
Loading