diff --git a/.config/checkstyle/checkstyle.xml b/.config/checkstyle/checkstyle.xml
index a0d7f17..13d47b9 100644
--- a/.config/checkstyle/checkstyle.xml
+++ b/.config/checkstyle/checkstyle.xml
@@ -9,7 +9,7 @@
-
+
diff --git a/.config/checkstyle/suppressions.xml b/.config/checkstyle/suppressions.xml
index 16d385e..d68ded8 100644
--- a/.config/checkstyle/suppressions.xml
+++ b/.config/checkstyle/suppressions.xml
@@ -3,4 +3,9 @@
"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN"
"https://checkstyle.org/dtds/suppressions_1_2.dtd">
+
+
+
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index a61d834..524e938 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -15,9 +15,9 @@ body:
attributes:
label: "Checklist"
options:
- - label: "I am able to reproduce the bug with the [latest version](https://github.com/xdev-software/template-placeholder/releases/latest)"
+ - label: "I am able to reproduce the bug with the [latest version](https://github.com/xdev-software/intellij-plugin-save-actions/releases/latest)"
required: true
- - label: "I made sure that there are *no existing issues* - [open](https://github.com/xdev-software/template-placeholder/issues) or [closed](https://github.com/xdev-software/template-placeholder/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
+ - label: "I made sure that there are *no existing issues* - [open](https://github.com/xdev-software/intellij-plugin-save-actions/issues) or [closed](https://github.com/xdev-software/intellij-plugin-save-actions/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
required: true
- label: "I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise."
required: true
diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml
index 764cae1..73ddf5b 100644
--- a/.github/ISSUE_TEMPLATE/enhancement.yml
+++ b/.github/ISSUE_TEMPLATE/enhancement.yml
@@ -13,7 +13,7 @@ body:
attributes:
label: "Checklist"
options:
- - label: "I made sure that there are *no existing issues* - [open](https://github.com/xdev-software/template-placeholder/issues) or [closed](https://github.com/xdev-software/template-placeholder/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
+ - label: "I made sure that there are *no existing issues* - [open](https://github.com/xdev-software/intellij-plugin-save-actions/issues) or [closed](https://github.com/xdev-software/intellij-plugin-save-actions/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
required: true
- label: "I have taken the time to fill in all the required details. I understand that the feature request will be dismissed otherwise."
required: true
diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml
index 6ecd6ad..173a4a4 100644
--- a/.github/ISSUE_TEMPLATE/question.yml
+++ b/.github/ISSUE_TEMPLATE/question.yml
@@ -12,7 +12,7 @@ body:
attributes:
label: "Checklist"
options:
- - label: "I made sure that there are *no existing issues* - [open](https://github.com/xdev-software/template-placeholder/issues) or [closed](https://github.com/xdev-software/template-placeholder/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
+ - label: "I made sure that there are *no existing issues* - [open](https://github.com/xdev-software/intellij-plugin-save-actions/issues) or [closed](https://github.com/xdev-software/intellij-plugin-save-actions/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
required: true
- label: "I have taken the time to fill in all the required details. I understand that the question will be dismissed otherwise."
required: true
diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml
index 5c51cfd..0b9e5e8 100644
--- a/.github/workflows/check-build.yml
+++ b/.github/workflows/check-build.yml
@@ -71,7 +71,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: plugin-files-java-${{ matrix.java }}
- path: build/libs/template-placeholder-*.jar
+ path: build/libs/intellij-plugin-save-actions-*.jar
if-no-files-found: error
checkstyle:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 808ebdf..701a63d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -92,7 +92,7 @@ jobs:
See [Changelog#v${{ steps.version.outputs.release }}](https://github.com/xdev-software/${{ github.event.repository.name }}/blob/develop/CHANGELOG.md#${{ steps.version.outputs.releasenumber }}) for more information.
## Installation
- The plugin is listed on the [Marketplace](https://plugins.jetbrains.com/plugin/pluginId).
+ The plugin is listed on the [Marketplace](https://plugins.jetbrains.com/plugin/22113).
Open the plugin Marketplace in your IDE (``File > Settings > Plugins > Marketplace``), search for the plugin and hit the install button.
diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml
index b52c3e2..20728c8 100644
--- a/.idea/checkstyle-idea.xml
+++ b/.idea/checkstyle-idea.xml
@@ -17,4 +17,4 @@
-
\ No newline at end of file
+
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 19681fa..21e0aff 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -96,4 +96,4 @@
-
+
\ No newline at end of file
diff --git a/.idea/saveactions_settings.xml b/.idea/saveactions_settings.xml
index 848c311..0b06f2f 100644
--- a/.idea/saveactions_settings.xml
+++ b/.idea/saveactions_settings.xml
@@ -17,5 +17,11 @@
+
\ No newline at end of file
diff --git a/.run/Run Verifications.run.xml b/.run/Run Verifications.run.xml
index 32783f5..b51e78c 100644
--- a/.run/Run Verifications.run.xml
+++ b/.run/Run Verifications.run.xml
@@ -22,4 +22,4 @@
false
-
\ No newline at end of file
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e69de29..619fc11 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -0,0 +1,68 @@
+## 1.4.0
+* Dropped support for IntelliJ versions < 2024.3
+ * This is required to fix a few deprecations and remove some workarounds #171
+
+## 1.3.1
+* Fix IDE hang when projects with different "Process files asynchronously" are open #160
+
+## 1.3.0
+* Make it possible to run processors asynchronously #130
+ * This way the UI should be more responsive when processing a lot of files
+ * May break processors that interact with the UI e.g. when showing dialogs
+* Don't process files during project load #145
+ * This should cause less race conditions due to partial project initialization
+ * Only active on IntelliJ < 2024.3 as [the underlying problem was fixed in IntelliJ 2024.3](https://github.com/JetBrains/intellij-community/commit/765caa71175d0a67a54836cf840fae829da590d9)
+
+## 1.2.4
+* Dropped support for IntelliJ versions < 2024.2
+* Removed deprecated code that was only required for older IDE versions
+
+## 1.2.3
+* Fix "run on multiple files" not working when the file is not a text file #129
+
+## 1.2.2
+* Workaround scaling problem on "New UI" [#26](https://github.com/xdev-software/intellij-plugin-template/issues/26)
+
+## 1.2.1
+* Fixed ``ToggleAnAction must override getActionUpdateThread`` warning inside IntelliJ 2024+
+* Dropped support for IntelliJ versions < 2023.2
+
+## 1.2.0
+* Run GlobalProcessors (e.g. Reformat) last so that code is formatted correctly #90
+* Dropped support for IntelliJ versions < 2023
+
+## 1.1.1
+* Shortened plugin name - new name: "Save Actions X"
+* Updated assets
+
+## 1.1.0
+* Removed "Remove unused suppress warning annotation"
+ * This option never worked #64
+ * Allows usage of the plugin with IntelliJ IDEA 2024+ #63
+ * If you used this option you should remove the line ``
`` inside ``saveactions_settings.xml``
+* Allow compilation with Java 21
+
+## 1.0.5
+* Fixed ``Add class qualifier to static member access outside declaring class`` not working in combination with Qodana plugin #25
+
+## 1.0.4
+* Fixed pluginIcon being not displayed #35
+* Improved support of Android Studio (until a 2023 version is released) #27
+
+## 1.0.3
+* Fixed problem in combination with Qodana plugin #25
+* Improved compatibility and cleaned up code #27
+
+## 1.0.2
+* Fixed missing display name which causes an error when multiple configurable plugins are installed #20
+
+## 1.0.1
+* Fixed ``Change visibility of field or method to lower access`` not working #14
+
+## 1.0.0
+Initial release
+* Fork of [dubreuia/intellij-plugin-save-actions](https://github.com/dubreuia/intellij-plugin-save-actions) and [fishermans/intellij-plugin-save-actions](https://github.com/fishermans/intellij-plugin-save-actions)
+ * ⚠️ This plugin is not compatible with the old/deprecated/forked one. Please ensure that the old plugin is uninstalled.
+* Rebrand
+* Updated copy pasted classes from IDEA
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index fe4debe..2080d16 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -46,17 +46,19 @@ Start idea and import the `build.gradle` file with "File > Open". Then in the "I
./gradlew cleanIdea idea
```
-IntelliJ should refresh and the project is now configured as a gradle project. You can find IntelliJ gradle tasks in "Gradle > Gradle projects > template-placeholder > Tasks > intellij". To run the plugin, use the `runIde` task:
+IntelliJ should refresh and the project is now configured as a gradle project. You can find IntelliJ gradle tasks in "Gradle > Gradle projects > intellij-plugin-save-actions > Tasks > intellij". To run the plugin, use the `runIde` task:
```bash
# Run the plugin (starts new idea)
./gradlew runIde
```
-## Releasing [![Build](https://img.shields.io/github/actions/workflow/status/xdev-software/template-placeholder/release.yml?branch=master)](https://github.com/xdev-software/template-placeholder/actions/workflows/release.yml)
+Based on the [original documentation](https://github.com/dubreuia/intellij-plugin-save-actions/blob/main/CONTRIBUTING.md)
+
+## Releasing [![Build](https://img.shields.io/github/actions/workflow/status/xdev-software/intellij-plugin-save-actions/release.yml?branch=master)](https://github.com/xdev-software/intellij-plugin-save-actions/actions/workflows/release.yml)
Before releasing:
-* Consider doing a [test-deployment](https://github.com/xdev-software/template-placeholder/actions/workflows/test-deploy.yml?query=branch%3Adevelop) before actually releasing.
+* Consider doing a [test-deployment](https://github.com/xdev-software/intellij-plugin-save-actions/actions/workflows/test-deploy.yml?query=branch%3Adevelop) before actually releasing.
* Check the [changelog](CHANGELOG.md)
If the ``develop`` is ready for release, create a pull request to the ``master``-Branch and merge the changes
diff --git a/LICENSE b/LICENSE
index ccaa2b3..3568158 100644
--- a/LICENSE
+++ b/LICENSE
@@ -187,6 +187,8 @@
identification within third-party archives.
Copyright 2024 XDEV Software
+ Copyright 2023 fishermans
+ Copyright 2020 Alexandre DuBreuil
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index 0d8c240..78636b8 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,64 @@
-[![Latest version](https://img.shields.io/jetbrains/plugin/v/pluginId?logo=jetbrains)](https://plugins.jetbrains.com/plugin/pluginId)
-[![Build](https://img.shields.io/github/actions/workflow/status/xdev-software/template-placeholder/check-build.yml?branch=develop)](https://github.com/xdev-software/template-placeholder/actions/workflows/check-build.yml?query=branch%3Adevelop)
-[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=xdev-software_template-placeholder&metric=alert_status)](https://sonarcloud.io/dashboard?id=xdev-software_template-placeholder)
-[![Feel free to leave a rating](https://img.shields.io/jetbrains/plugin/r/rating/pluginId?style=social&logo=jetbrains&label=Feel%20free%20to%20leave%20a%20rating)](https://plugins.jetbrains.com/plugin/pluginId/reviews)
+[![Latest version](https://img.shields.io/jetbrains/plugin/v/22113?logo=jetbrains)](https://plugins.jetbrains.com/plugin/22113)
+[![Build](https://img.shields.io/github/actions/workflow/status/xdev-software/intellij-plugin-save-actions/check-build.yml?branch=develop)](https://github.com/xdev-software/intellij-plugin-save-actions/actions/workflows/check-build.yml?query=branch%3Adevelop)
+[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=xdev-software_intellij-plugin-save-actions&metric=alert_status)](https://sonarcloud.io/dashboard?id=xdev-software_intellij-plugin-save-actions)
+[![Feel free to leave a rating](https://img.shields.io/jetbrains/plugin/r/rating/22113?style=social&logo=jetbrains&label=Feel%20free%20to%20leave%20a%20rating)](https://plugins.jetbrains.com/plugin/22113/reviews)
-# template-placeholder
+# Save Actions X
+> [!NOTE]
+> This plugin is a fork of [dubreuia/intellij-plugin-save-actions](https://github.com/dubreuia/intellij-plugin-save-actions) and [fishermans/intellij-plugin-save-actions](https://github.com/fishermans/intellij-plugin-save-actions) and is kept in maintenance mode:
+> * Keep the plugin up-to-date with the latest IDEA versions
+> * Distribute the plugin on the IDEA marketplace
+> * Fix serious bugs
+> * Keep the repo in sync with XDEV's standards
+> * Hardly used features may be removed to speed up development
+>
+> There is no guarantee that work outside of this scope will be done.
+Supports configurable, Eclipse like, save actions, including "optimize imports", "reformat code", "rearrange code", "compile file" and some quick fixes like "add / remove 'this' qualifier", etc. The plugin executes the configured actions when the file is synchronized (or saved) on disk.
+
+Using the save actions plugin makes your code cleaner and more uniform across your code base by enforcing your code style and code rules every time you save. The settings file (see [files location](./USAGE.md#files-location)) can be shared in your development team so that every developer has the same configuration.
+
+The code style applied by the save actions plugin is the one configured your settings at "File > Settings > Editor > Code Style". For some languages, custom formatter (Dartfmt, Prettier, etc.) may also be triggered by the save actions plugin. See the [Editor Actions](./USAGE.md#editor-actions) configuration for more information.
+
+## Features
+
+### All JetBrains products
+
+- Optimize imports
+- Run on file save, shortcut, batch (or a combination)
+- Run on multiple files by choosing a scope
+- Reformat code (whole file or only changed text)
+- Rearrange code (reorder methods, fields, etc.)
+- Include / exclude files with regex support
+- Works on any file type (Java, Python, XML, etc.)
+- Launch any editor action using "quick lists"
+- Uses a settings file per project you can commit (see [Files location](./USAGE.md#files-location))
+- Available keymaps and actions for activation (see [Keymap and actions](./USAGE.md#keymap-and-actions))
+
+
+
+### Java IDE products
+
+Works in JetBrains IDE with Java support, like Intellij IDEA and AndroidStudio.
+
+- Compile project after save (if compiling is available)
+- Reload debugger after save (if compiling is available)
+- Eclipse configuration file `.epf` support (see [Eclipse support](./USAGE.md#eclipse-support))
+- Automatically fix Java inspections (see [Java quick fixes](./USAGE.md#java-fixes))
+
+
## Installation
-[Installation guide for the latest release](https://github.com/xdev-software/template-placeholder/releases/latest#Installation)
+[Installation guide for the latest release](https://github.com/xdev-software/intellij-plugin-save-actions/releases/latest#Installation)
> [!TIP]
-> [Development versions](https://plugins.jetbrains.com/plugin/pluginId/versions/snapshot) can be installed by [adding the ``snapshot`` release channel as a plugin repository](https://www.jetbrains.com/help/idea/managing-plugins.html#repos):
+> [Development versions](https://plugins.jetbrains.com/plugin/22113/versions/snapshot) can be installed by [adding the ``snapshot`` release channel as a plugin repository](https://www.jetbrains.com/help/idea/managing-plugins.html#repos):
> ``https://plugins.jetbrains.com/plugins/snapshot/list``
+## Usage
+
+Read the [full usage guide here](./USAGE.md).
+
## Contributing
See the [contributing guide](./CONTRIBUTING.md) for detailed instructions on how to get started with our project.
diff --git a/SECURITY.md b/SECURITY.md
index 34b9514..970016c 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,4 +2,4 @@
## Reporting a Vulnerability
-Please report a security vulnerability [on GitHub Security Advisories](https://github.com/xdev-software/template-placeholder/security/advisories/new).
+Please report a security vulnerability [on GitHub Security Advisories](https://github.com/xdev-software/intellij-plugin-save-actions/security/advisories/new).
diff --git a/USAGE.md b/USAGE.md
new file mode 100644
index 0000000..06a60f6
--- /dev/null
+++ b/USAGE.md
@@ -0,0 +1,157 @@
+## Usage
+
+The plugin can trigger automatically or manually on IDE actions (standard actions) or plugin actions. Most actions needs to be enabled individually (see [activation](#activation)).
+
+### IDE actions
+
+The plugin will trigger automatically on any of these IDE actions (needs to be activated with "Activate save actions on file save" in [activation](#activation)):
+
+- **Frame deactivation**, which is when the editor loses focus, configured in "File > Settings > Appearance & Behavior > System Settings > Save files on frame deactivation" .
+- **Application idle**, which is when the IDE isn't used for a period of time, configured in "File > Settings > Appearance & Behavior > System Settings > Save files automatically if application is idle for x sec".
+- **Save All**, which is bound by default to "CTRL + S". Some IDE might use "CTRL + S" for the single **Save Document** action, on which the plugin will NOT trigger. This is by design, see issue [#222](https://github.com/dubreuia/intellij-plugin-save-actions/issues/222).
+
+### Plugin actions
+
+The plugin actions are grouped under the menu "Code > Save Actions". You can associate a keymap to any action in "Settings > Keymap > Search 'save actions'".
+
+- **Enable Save Actions (default: not binded)** will activate or deactivate the plugin by changing the configuration.
+- **Execute Save Actions on shortcut (default: "CTRL + SHIFT + S")** will trigger the plugin manually (needs to be activated with "Activate save actions on shortcut" in [activation](#activation)).
+- **Execute Save Actions on multiple files (default: not binded)** will show a popup to select the files (or a scope) on which to trigger the plugin (needs to be activated with "Activate save actions on batch" in [activation](#activation)).
+
+## Configuration
+
+The configurations are located in "File > Settings > Other Settings > Save Actions".
+
+### Activation
+
+You can quickly toggle the plugin activation by using the "Enable Save Action" action. Use "CTRL + SHIFT + A" then search for it. It will also show if it is currently activated or not.
+
+| Name | Description
+| --- | ---
+| Activate save actions on file save | Enable / disable the plugin on file save. Before saving each file, it will perform the configured actions below
+| Activate save actions on shortcut | Enable / disable the plugin on shortcut, by default "CTRL + SHIFT + S" (configured in "File > Keymaps > Main menu > Code > Save Actions")
+| Activate save actions on batch | Enable / disable the plugin on batch, by using "Code > Save Actions > Execute on multiple files"
+| No action if compile errors | Enable / disable no action if there are compile errors. Applied to each file individually
+| Process files asynchronously | Enable / disable if files should be processed asynchronously. Enabling it will result in less UI hangs but may break if a processor needs the UI.
+
+### Global
+
+| Name | Description
+| --- | ---
+| Optimize imports | Enable / disable import organization (configured in "File > Settings > Code Style > Java > Imports")
+| Reformat file | Enable / disable formatting (configured in "File > Settings > Code Style"). See "Reformat only changed code" for more options
+| Reformat only changed lines | Enable / disable formatting for only changed lines, which will work only if a VCS is configured
+| Rearrange fields and methods | Enable / disable re-ordering of fields and methods (configured in "File > Settings > Code Style > Java > Arrangement")
+
+### Build
+
+| Name | Description
+| --- | ---
+| *\[experimental\]* Compile file | Enable / disable compiling of the modified file. The compiler might compile other files as well. **Warning: this feature is experimental, please post feedback in the github issues**
+| *\[experimental\]* Reload file | Enable / disable reloading of the files in the running debugger, meaning the files will get compiled first. The compiler might compile other files as well. **Warning: this feature is experimental, please post feedback in the github issues**
+| *\[experimental\]* Execute action | Enable / disable executing of an action using quick lists (using quick lists at "File > Settings > Appearance & Behavior > Quick Lists"). See [Editor Actions](#editor-actions) for more information **Warning: this feature is experimental, please post feedback in the github issues**
+
+#### Editor Actions
+
+Some languages requires specific actions, such as Dartfmt or Prettier:
+
+- For Dart developers, enable "Use the dartfmt tool when formatting the whole file" option in "File > Settings > Editor > Code Style > Dart > Dartfmt".
+- For [Prettier](https://prettier.io/) users, read below.
+
+Using the "Execute action" configuration, the plugin can launch arbitrary editor actions. While not all actions will work, it can be used to launch external tools, specific runs, etc. This feature is experimental, you can post your feedback on issue [#118](https://github.com/dubreuia/intellij-plugin-save-actions/issues/118).
+
+The actions are implemented in the form of "quick lists", an IDE function that is used to define a list of actions that can be then executed. Quick lists can be configured at "File > Settings > Appearance & Behavior > Quick Lists", and once configured, one can be selected and used in the plugin, using the "Execution action" configuration drop down list.
+
+### File
+
+| Name | Description
+| --- | ---
+| File path inclusions | Add / remove file path inclusions (by default, everything included). The Java regular expressions match the whole file name from the project root. Include only Java files: `.*\.java`.
+| File path exclusions | Add / remove file path exclusions to ignore files (overrides inclusions). The Java regular expressions match the whole file name from the project root. Exclude 'Main.java' only in root folder: `Main\.java`. Exclude file 'Foo.java' only in folder 'src': `src/Foo\.java`. Exclude all xml files in any folder: `.*/.*\.xml`
+| Use external Eclipse configuration | Add external configuration file ".epf" to read settings from. This will update the current settings and use only the ".epf" file content. Use "reset" button to remove
+
+### Java fixes
+
+If a quick fix adds something that is removed by another quick fix, the removal wins.
+
+| Name | Description
+| --- | ---
+| Add final modifier to field | The field `private int field = 0` becomes `private final int field = 0`
+| Add final modifier to local variable or parameter | The local variable `int variable = 0` becomes `final int variable = 0`
+| Add final modifier to local variable or parameter except if implicit | The local variable `int variable = 0` becomes `final int variable = 0`, but not if it is implicit like in try with resources `try (Resource r = new Resource())`
+| Add static modifier to methods | The method `private void method()` becomes `private static void method()` if the content does not references instance fields
+| Add this to field access | The access to instance field `field = 0` becomes `this.field = 0`
+| Add this to method access | The access to instance method `method()` becomes `this.method()`
+| Add class qualifier to static member access | The access to class field `FIELD = 0` becomes `Class.FIELD` for a class named Class. Exclusive with "Add class qualifier to static member access outside declaring class only".
+| Add class qualifier to static member access outside declaring class only | The access to class field `FIELD = 0` becomes `Class.FIELD` for a class named class, but only if the static member is outside declaring class. Exclusive with "Add class qualifier to static member access".
+| Add missing @Override annotations | The method `void method()` becomes `@Override void method()` if it overrides a method from the parent class
+| Add blocks to if/while/for statements | The statement `if (true) return false` becomes `if (true) { return false; }` (a block), also working for `for` and `while` statements
+| Add missing serialVersionUID field for Serializable classes | The class `class Class implements Serializable` will get a new field `private static final long serialVersionUID` with generated serial version uid
+| Remove unnecessary this to field and method | The access to instance field `this.field = 0` becomes `field = 0`, also working for methods
+| Remove final from private method | The method `private final void method()` becomes `private void method()`
+| Remove unnecessary final to local variable or parameter | The local variable `int final variable = 0` becomes `int variable = 0`
+| Remove explicit generic type for diamond | The list creation `List list = new ArrayList()` becomes `List list = new ArrayList<>()`
+| Remove unnecessary semicolon | The statement `int variable = 0;;` becomes `int variable = 0;`
+| Remove blocks from if/while/for statements | The statement `if (true) { return false; }` becomes `if (true) return false;`, also working for `for` and `while` statements
+| Change visibility of field or method to lower access | The field `public int field = 0` becomes `private int field = 0` if it is not used outside class, also working for methods
+
+## Compatibility
+
+The plugin will be kept compatible with the latest IDEA version.
+
+Support for other JetBrains products should also be automatically available but is not guaranteed.
+
+### Eclipse configuration support
+
+The save-actions plugin supports Eclipse configuration `.epf` files by the [Workspace Mechanic](https://marketplace.eclipse.org/content/workspace-mechanic) Eclipse plugin (Java IDE only).
+You can specify a path to an Eclipse configuration file in the "Eclipse support" settings section to import it.
+The plugin will load the content of the file and configure the corresponding options.
+Please note that not all plugin options have corresponding ``.epf`` options and therefore might not be configured.
+Use the "reset" button to remove the import.
+
+The plugin will stay in sync with your Eclipse configuration file. Not every features are present on either side, but the ones that are in common are supported.
+
+You can find an example of [an Eclipse configuration `.epf` file](src/test/resources/software/xdev/saveactions/model) in the test resources.
+
+```properties
+# @title Save Actions
+# @description Save Actions
+# @task_type LASTMOD
+file_export_version=3.0
+/instance/org.eclipse.jdt.ui/editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
+/instance/org.eclipse.jdt.ui/sp_cleanup.format_source_code=true
+/instance/org.eclipse.jdt.ui/sp_cleanup.format_source_code_changes_only=false
+/instance/org.eclipse.jdt.ui/sp_cleanup.organize_imports=true
+/instance/org.eclipse.jdt.ui/sp_cleanup.remove_trailing_whitespaces=true
+...
+```
+
+### Other plugin compatibility
+
+Some things to note when using other plugins with the Save Actions plugin:
+
+- [IdeaVim](https://plugins.jetbrains.com/plugin/164-ideavim): Since the Save Actions plugin do not trigger on the "Save Document" action (see [usage](#usage)), using `:w` to save in IdeaVim won't trigger the plugin, but using `:wa` will, since it calls the "Save All" action. See issue [#222](https://github.com/dubreuia/intellij-plugin-save-actions/issues/222)).
+- [detekt](https://plugins.jetbrains.com/plugin/10761-detekt): Using the detekt plugin breaks the Save Actions plugin, see issue [#270](https://github.com/dubreuia/intellij-plugin-save-actions/issues/270).
+
+## Files location
+
+- **idea.log**: The log file the save actions plugin writes in. It contains debug information, prefixed with `software.xdev.saveactions.SaveActionManager`. If you are using default locations, it would be in `~/.IntelliJIdeaVERSION/system/log/idea.log`.
+- **saveactions_settings.xml**: The settings file is saved by project in the `.idea` folder. That file can be committed in git thus shared in your development team. If you are using the default locations, it would be in `~/IdeaProjects/PROJECT_NAME/.idea/saveactions_settings.xml`
+
+## Troubleshooting of common problems
+
+### "Conflicting component name 'SaveActionSettings': class ``com.dubreuia.model.Storage`` and class ``software.xdev.saveactions.model.Storage``"
+
+The problem only happens when the [old/deprecated/forked plugin](https://github.com/dubreuia/intellij-plugin-save-actions) plugin is also installed.
+
+You can fix this by uninstalling the deprecated plugin.
+
+### "AWT events are not allowed inside write action" occurs when applying Save Actions
+
+This usually indicates that some action causes a UI dialog to show up. However as the actions are run in the background the dialog can't be shown and the crash occurs.
+
+You can work around this problem by finding out what causes the dialog (e.g. by trying to temporarily disabling Save Actions and saving the files normally) and stop it from being displayed.
+
+### "Execute Save Actions on multiple files" is not working
+
+Make sure that you enabled "Activate save actions on batch" in the settings.
diff --git a/assets/intellij-save-actions-plugin-settings-page-java.png b/assets/intellij-save-actions-plugin-settings-page-java.png
new file mode 100644
index 0000000..49b0705
Binary files /dev/null and b/assets/intellij-save-actions-plugin-settings-page-java.png differ
diff --git a/assets/intellij-save-actions-plugin-settings-page.png b/assets/intellij-save-actions-plugin-settings-page.png
new file mode 100644
index 0000000..f359a92
Binary files /dev/null and b/assets/intellij-save-actions-plugin-settings-page.png differ
diff --git a/build.gradle b/build.gradle
index 3f967a9..ab6bed1 100644
--- a/build.gradle
+++ b/build.gradle
@@ -44,7 +44,7 @@ configurations.checkstyle {
}
}
-// Add dependencies to test, junit5 api (annotations) and engine (runtime)
+import org.jetbrains.intellij.platform.gradle.TestFrameworkType
dependencies {
intellijPlatform {
create(properties("platformType"), properties("platformVersion"))
@@ -53,6 +53,7 @@ dependencies {
pluginVerifier()
zipSigner()
instrumentationTools()
+ testFramework TestFrameworkType.Platform.INSTANCE
}
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
pmd "net.sourceforge.pmd:pmd-ant:${pmdVersion}",
diff --git a/gradle.properties b/gradle.properties
index dc95cd9..9b41416 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,15 +1,15 @@
# IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html
-pluginGroup=
-pluginName=
+pluginGroup=software.xdev.saveactions
+pluginName=Save Actions X
# SemVer format -> https://semver.org
-pluginVersion=
+pluginVersion=1.4.1-SNAPSHOT
# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
platformType=IC
platformVersion=2024.3
platformSinceBuild=243
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
# Example: platformBundledPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
-platformBundledPlugins=
+platformBundledPlugins=com.intellij.java
platformPlugins=
# Gradle Releases -> https://github.com/gradle/gradle/releases
gradleVersion=8.10.1
diff --git a/src/main/java/software/xdev/saveactions/core/ExecutionMode.java b/src/main/java/software/xdev/saveactions/core/ExecutionMode.java
new file mode 100644
index 0000000..360a4d4
--- /dev/null
+++ b/src/main/java/software/xdev/saveactions/core/ExecutionMode.java
@@ -0,0 +1,34 @@
+package software.xdev.saveactions.core;
+
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+
+
+public enum ExecutionMode
+{
+
+ /**
+ * When the plugin is called normally (the IDE calls the plugin component on frame deactivation or "save all"). The
+ * {@link #saveSingle} is also called on every document.
+ *
+ * @see FileDocumentManager#saveAllDocuments()
+ */
+ saveAll,
+
+ /**
+ * When the plugin is called only with a single save (some other plugins like ideavim do that).
+ *
+ * @see FileDocumentManager#saveDocument(Document)
+ */
+ saveSingle,
+
+ /**
+ * When the plugin is called in batch mode (the IDE calls the plugin after a file selection popup).
+ */
+ batch,
+
+ /**
+ * When the plugin is called from a user input shortcut.
+ */
+ shortcut,
+}
diff --git a/src/main/java/software/xdev/saveactions/core/action/BatchAction.java b/src/main/java/software/xdev/saveactions/core/action/BatchAction.java
new file mode 100644
index 0000000..d65bec8
--- /dev/null
+++ b/src/main/java/software/xdev/saveactions/core/action/BatchAction.java
@@ -0,0 +1,59 @@
+package software.xdev.saveactions.core.action;
+
+import static java.util.Collections.synchronizedSet;
+import static software.xdev.saveactions.core.ExecutionMode.batch;
+import static software.xdev.saveactions.model.Action.activateOnBatch;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.jetbrains.annotations.NotNull;
+
+import com.intellij.analysis.AnalysisScope;
+import com.intellij.analysis.BaseAnalysisAction;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.PsiFile;
+
+import software.xdev.saveactions.core.service.SaveActionsService;
+import software.xdev.saveactions.core.service.SaveActionsServiceManager;
+import software.xdev.saveactions.model.Action;
+
+
+/**
+ * This action runs the save actions on the given scope of files, only if property {@link Action#activateOnShortcut} is
+ * enabled. The user is asked for the scope using a standard IDEA dialog. It delegates to {@link SaveActionsService}.
+ * Originally based on {@link com.intellij.codeInspection.inferNullity.InferNullityAnnotationsAction}.
+ *
+ * @author markiewb
+ * @see SaveActionsServiceManager
+ */
+public class BatchAction extends BaseAnalysisAction
+{
+ private static final Logger LOGGER = Logger.getInstance(SaveActionsService.class);
+ private static final String COMPONENT_NAME = "Save Actions";
+
+ public BatchAction()
+ {
+ super(COMPONENT_NAME, COMPONENT_NAME);
+ }
+
+ @Override
+ protected void analyze(@NotNull final Project project, @NotNull final AnalysisScope scope)
+ {
+ LOGGER.info("[+] Start BatchAction#analyze with project " + project + " and scope " + scope);
+ final Set psiFiles = synchronizedSet(new HashSet<>());
+ scope.accept(new PsiElementVisitor()
+ {
+ @Override
+ public void visitFile(final PsiFile psiFile)
+ {
+ super.visitFile(psiFile);
+ psiFiles.add(psiFile);
+ }
+ });
+ SaveActionsServiceManager.getService().guardedProcessPsiFiles(project, psiFiles, activateOnBatch, batch);
+ LOGGER.info("End BatchAction#analyze processed " + psiFiles.size() + " files");
+ }
+}
diff --git a/src/main/java/software/xdev/saveactions/core/action/ShortcutAction.java b/src/main/java/software/xdev/saveactions/core/action/ShortcutAction.java
new file mode 100644
index 0000000..9fc835c
--- /dev/null
+++ b/src/main/java/software/xdev/saveactions/core/action/ShortcutAction.java
@@ -0,0 +1,44 @@
+package software.xdev.saveactions.core.action;
+
+import static com.intellij.openapi.actionSystem.CommonDataKeys.PSI_FILE;
+import static java.util.Collections.singletonList;
+import static software.xdev.saveactions.core.ExecutionMode.shortcut;
+import static software.xdev.saveactions.model.Action.activateOnShortcut;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.jetbrains.annotations.NotNull;
+
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiFile;
+
+import software.xdev.saveactions.core.service.SaveActionsService;
+import software.xdev.saveactions.core.service.SaveActionsServiceManager;
+import software.xdev.saveactions.model.Action;
+
+
+/**
+ * This action runs the plugin on shortcut, only if property {@link Action#activateOnShortcut} is enabled. It delegates
+ * to {@link SaveActionsService}.
+ *
+ * @see SaveActionsServiceManager
+ */
+public class ShortcutAction extends AnAction
+{
+ private static final Logger LOGGER = Logger.getInstance(SaveActionsService.class);
+
+ @Override
+ public void actionPerformed(@NotNull final AnActionEvent event)
+ {
+ LOGGER.info("[+] Start ShortcutAction#actionPerformed with event " + event);
+ final PsiFile psiFile = event.getData(PSI_FILE);
+ final Project project = event.getProject();
+ final Set psiFiles = new HashSet<>(singletonList(psiFile));
+ SaveActionsServiceManager.getService().guardedProcessPsiFiles(project, psiFiles, activateOnShortcut, shortcut);
+ LOGGER.info("End ShortcutAction#actionPerformed processed " + psiFiles.size() + " files");
+ }
+}
diff --git a/src/main/java/software/xdev/saveactions/core/action/ToggleAnAction.java b/src/main/java/software/xdev/saveactions/core/action/ToggleAnAction.java
new file mode 100644
index 0000000..5da4446
--- /dev/null
+++ b/src/main/java/software/xdev/saveactions/core/action/ToggleAnAction.java
@@ -0,0 +1,49 @@
+package software.xdev.saveactions.core.action;
+
+import static software.xdev.saveactions.model.Action.activate;
+
+import org.jetbrains.annotations.NotNull;
+
+import com.intellij.openapi.actionSystem.ActionUpdateThread;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.ToggleAction;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.Project;
+
+import software.xdev.saveactions.model.Storage;
+
+
+/**
+ * This action toggles on and off the plugin, by modifying the underlying storage.
+ */
+public class ToggleAnAction extends ToggleAction implements DumbAware
+{
+ @Override
+ public boolean isSelected(final AnActionEvent event)
+ {
+ final Project project = event.getProject();
+ if(project != null)
+ {
+ final Storage storage = project.getService(Storage.class);
+ return storage.isEnabled(activate);
+ }
+ return false;
+ }
+
+ @Override
+ public void setSelected(final AnActionEvent event, final boolean state)
+ {
+ final Project project = event.getProject();
+ if(project != null)
+ {
+ final Storage storage = project.getService(Storage.class);
+ storage.setEnabled(activate, state);
+ }
+ }
+
+ @Override
+ public @NotNull ActionUpdateThread getActionUpdateThread()
+ {
+ return ActionUpdateThread.BGT;
+ }
+}
diff --git a/src/main/java/software/xdev/saveactions/core/component/Engine.java b/src/main/java/software/xdev/saveactions/core/component/Engine.java
new file mode 100644
index 0000000..5687fc9
--- /dev/null
+++ b/src/main/java/software/xdev/saveactions/core/component/Engine.java
@@ -0,0 +1,366 @@
+package software.xdev.saveactions.core.component;
+
+import static java.util.stream.Collectors.toSet;
+
+import java.util.AbstractMap.SimpleEntry;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import org.jetbrains.annotations.NotNull;
+
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.application.WriteAction;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ProjectRootManager;
+import com.intellij.openapi.util.ThrowableComputable;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiFile;
+import com.intellij.util.ApplicationKt;
+import com.intellij.util.PsiErrorElementUtil;
+import com.intellij.util.ThrowableRunnable;
+
+import software.xdev.saveactions.core.ExecutionMode;
+import software.xdev.saveactions.core.service.SaveActionsService;
+import software.xdev.saveactions.model.Action;
+import software.xdev.saveactions.model.Storage;
+import software.xdev.saveactions.processors.Processor;
+import software.xdev.saveactions.processors.Result;
+import software.xdev.saveactions.processors.ResultCode;
+import software.xdev.saveactions.processors.SaveCommand;
+
+
+/**
+ * Implementation of the save action engine. This class will filter, process and log modifications to the files.
+ */
+public class Engine
+{
+ private static final Logger LOGGER = Logger.getInstance(SaveActionsService.class);
+ private static final String REGEX_STARTS_WITH_ANY_STRING = ".*?";
+
+ private final Storage storage;
+ private final List processors;
+ private final Project project;
+ private final Set psiFiles;
+ private final Action activation;
+ private final ExecutionMode mode;
+
+ public Engine(
+ final Storage storage,
+ final List processors,
+ final Project project,
+ final Set psiFiles,
+ final Action activation,
+ final ExecutionMode mode)
+ {
+ this.storage = storage;
+ this.processors = processors;
+ this.project = project;
+ this.psiFiles = psiFiles;
+ this.activation = activation;
+ this.mode = mode;
+ }
+
+ public void processPsiFilesIfNecessary(
+ @NotNull final ProgressIndicator indicator,
+ final boolean async)
+ {
+ if(this.psiFiles == null)
+ {
+ return;
+ }
+ if(!this.storage.isEnabled(this.activation))
+ {
+ LOGGER.info(String.format(
+ "Action \"%s\" not enabled on %s",
+ this.activation.getText(),
+ this.project.getName()));
+ return;
+ }
+
+ indicator.setIndeterminate(true);
+ final Set psiFilesEligible = this.getEligiblePsiFiles(indicator, async);
+ if(psiFilesEligible.isEmpty())
+ {
+ LOGGER.info("No files are eligible - " + this.project.getName());
+ return;
+ }
+
+ final List processorsEligible = this.getEligibleProcessors(indicator, psiFilesEligible);
+ if(processorsEligible.isEmpty())
+ {
+ LOGGER.info("No processors are eligible - " + this.project.getName());
+ return;
+ }
+
+ this.flushPsiFiles(indicator, async, psiFilesEligible);
+
+ this.execute(indicator, processorsEligible, psiFilesEligible);
+ }
+
+ private Set getEligiblePsiFiles(final @NotNull ProgressIndicator indicator, final boolean async)
+ {
+ LOGGER.info(String.format("Processing %s files %s mode %s", this.project.getName(), this.psiFiles, this.mode));
+ indicator.checkCanceled();
+ indicator.setText2("Collecting files to process");
+
+ final ThrowableComputable, RuntimeException> psiFilesEligibleFunc =
+ () -> this.psiFiles.stream()
+ .filter(psiFile -> this.isPsiFileEligible(this.project, psiFile))
+ .collect(toSet());
+ final Set psiFilesEligible = async
+ ? ReadAction.compute(psiFilesEligibleFunc)
+ : psiFilesEligibleFunc.compute();
+ LOGGER.info(String.format("Valid files %s", psiFilesEligible));
+ return psiFilesEligible;
+ }
+
+ private @NotNull List getEligibleProcessors(
+ final @NotNull ProgressIndicator indicator,
+ final Set psiFilesEligible)
+ {
+ LOGGER.info(String.format("Start processors (%d) - %s", this.processors.size(), this.project.getName()));
+ indicator.checkCanceled();
+ indicator.setText2("Collecting processors");
+
+ final List processorsEligible = this.processors.stream()
+ .map(processor -> processor.getSaveCommand(this.project, psiFilesEligible))
+ .filter(command -> this.storage.isEnabled(command.getAction()))
+ .filter(command -> command.getModes().contains(this.mode))
+ .toList();
+ LOGGER.info(String.format("Filtered processors %s", processorsEligible));
+ return processorsEligible;
+ }
+
+ private void flushPsiFiles(
+ final @NotNull ProgressIndicator indicator,
+ final boolean async,
+ final Set psiFilesEligible)
+ {
+ LOGGER.info(String.format("Flushing files (%d) - %s", psiFilesEligible.size(), this.project.getName()));
+ indicator.checkCanceled();
+ indicator.setText2("Flushing files");
+
+ final ThrowableRunnable flushFilesFunc = () -> {
+ final PsiDocumentManager psiDocumentManager = PsiDocumentManager.getInstance(this.project);
+ psiFilesEligible.forEach(psiFile -> this.commitDocumentAndSave(psiFile, psiDocumentManager));
+ };
+ if(async)
+ {
+ ApplicationKt.getApplication().invokeAndWait(() -> WriteAction.run(flushFilesFunc));
+ }
+ else
+ {
+ flushFilesFunc.run();
+ }
+ }
+
+ private void execute(
+ final @NotNull ProgressIndicator indicator,
+ final List processorsEligible,
+ final Set psiFilesEligible)
+ {
+ indicator.checkCanceled();
+ indicator.setIndeterminate(false);
+ indicator.setFraction(0d);
+
+ final List saveCommands = processorsEligible.stream()
+ .filter(Objects::nonNull)
+ .toList();
+
+ final AtomicInteger executedCount = new AtomicInteger();
+ final List>> results = saveCommands.stream()
+ .map(command -> {
+ LOGGER.info(String.format(
+ "Execute command %s on %d files - %s",
+ command,
+ psiFilesEligible.size(),
+ this.project.getName()));
+
+ indicator.checkCanceled();
+ indicator.setText2("Executing '" + command.getAction().getText() + "'");
+
+ final SimpleEntry> entry =
+ new SimpleEntry<>(command.getAction(), command.execute());
+
+ indicator.setFraction((double)executedCount.incrementAndGet() / saveCommands.size());
+
+ return entry;
+ })
+ .toList();
+ LOGGER.info(String.format(
+ "Exit engine with results %s - %s",
+ results.stream()
+ .map(entry -> entry.getKey() + ":" + entry.getValue())
+ .toList(),
+ this.project.getName()));
+ }
+
+ private boolean isPsiFileEligible(final Project project, final PsiFile psiFile)
+ {
+ return psiFile != null
+ && this.isProjectValid(project)
+ && this.isPsiFileValid(psiFile)
+ && this.hasPsiFileText(psiFile)
+ && this.isPsiFileFresh(psiFile)
+ && this.isPsiFileInProject(project, psiFile)
+ && this.isPsiFileNoError(project, psiFile)
+ && this.isPsiFileIncluded(psiFile);
+ }
+
+ private boolean isProjectValid(final Project project)
+ {
+ final boolean valid = project.isInitialized() && !project.isDisposed();
+ if(!valid)
+ {
+ LOGGER.info(String.format("Project %s invalid. Either not initialized or disposed", project.getName()));
+ }
+ return valid;
+ }
+
+ private boolean isPsiFileInProject(final Project project, @NotNull final PsiFile psiFile)
+ {
+ final boolean inProject =
+ ProjectRootManager.getInstance(project).getFileIndex().isInContent(psiFile.getVirtualFile());
+ if(!inProject)
+ {
+ LOGGER.info(String.format(
+ "File %s not in current project %s. File belongs to %s",
+ psiFile,
+ project,
+ psiFile.getProject()));
+ }
+ return inProject;
+ }
+
+ private boolean isPsiFileNoError(final Project project, final PsiFile psiFile)
+ {
+ if(this.storage.isEnabled(Action.noActionIfCompileErrors))
+ {
+ final boolean hasErrors = PsiErrorElementUtil.hasErrors(project, psiFile.getVirtualFile());
+ if(hasErrors)
+ {
+ LOGGER.info(String.format("File %s has errors", psiFile));
+ }
+ return !hasErrors;
+ }
+ return true;
+ }
+
+ private boolean isPsiFileIncluded(final PsiFile psiFile)
+ {
+ final String canonicalPath = psiFile.getVirtualFile().getCanonicalPath();
+ return this.isIncludedAndNotExcluded(canonicalPath);
+ }
+
+ private boolean isPsiFileFresh(final PsiFile psiFile)
+ {
+ if(this.mode == ExecutionMode.batch)
+ {
+ return true;
+ }
+ final boolean isFresh = psiFile.getModificationStamp() != 0;
+ if(!isFresh)
+ {
+ LOGGER.info(String.format("File %s is not fresh", psiFile));
+ }
+ return isFresh;
+ }
+
+ private boolean isPsiFileValid(final PsiFile psiFile)
+ {
+ final boolean valid = psiFile.isValid();
+ if(!valid)
+ {
+ LOGGER.info(String.format("File %s is not valid", psiFile));
+ }
+ return valid;
+ }
+
+ private boolean hasPsiFileText(final PsiFile psiFile)
+ {
+ final boolean valid = psiFile.getTextRange() != null;
+ if(!valid)
+ {
+ LOGGER.info(String.format("File %s has no text", psiFile));
+ }
+ return valid;
+ }
+
+ boolean isIncludedAndNotExcluded(final String path)
+ {
+ return this.isIncluded(path) && !this.isExcluded(path);
+ }
+
+ private boolean isExcluded(final String path)
+ {
+ final Set exclusions = this.storage.getExclusions();
+ final boolean psiFileExcluded = this.atLeastOneMatch(path, exclusions);
+ if(psiFileExcluded)
+ {
+ LOGGER.info(String.format("File %s excluded in %s", path, exclusions));
+ }
+ return psiFileExcluded;
+ }
+
+ private boolean isIncluded(final String path)
+ {
+ final Set inclusions = this.storage.getInclusions();
+ if(inclusions.isEmpty())
+ {
+ // If no inclusion are defined, all files are allowed
+ return true;
+ }
+ final boolean psiFileIncluded = this.atLeastOneMatch(path, inclusions);
+ if(psiFileIncluded)
+ {
+ LOGGER.info(String.format("File %s included in %s", path, inclusions));
+ }
+ return psiFileIncluded;
+ }
+
+ private boolean atLeastOneMatch(final String psiFileUrl, final Set patterns)
+ {
+ for(final String pattern : patterns)
+ {
+ try
+ {
+ final Matcher matcher = Pattern.compile(REGEX_STARTS_WITH_ANY_STRING + pattern).matcher(psiFileUrl);
+ if(matcher.matches())
+ {
+ return true;
+ }
+ }
+ catch(final PatternSyntaxException e)
+ {
+ // invalid patterns are ignored
+ return false;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Should properly fix #402 according to @krasa's recommendation in #109.
+ *
+ * @param psiFile of type PsiFile
+ */
+ private void commitDocumentAndSave(final PsiFile psiFile, final PsiDocumentManager psiDocumentManager)
+ {
+ final Document document = psiDocumentManager.getDocument(psiFile);
+ if(document != null)
+ {
+ psiDocumentManager.doPostponedOperationsAndUnblockDocument(document);
+ psiDocumentManager.commitDocument(document);
+ FileDocumentManager.getInstance().saveDocument(document);
+ }
+ }
+}
diff --git a/src/main/java/software/xdev/saveactions/core/listener/SaveActionsDocumentManagerListener.java b/src/main/java/software/xdev/saveactions/core/listener/SaveActionsDocumentManagerListener.java
new file mode 100644
index 0000000..2b7071d
--- /dev/null
+++ b/src/main/java/software/xdev/saveactions/core/listener/SaveActionsDocumentManagerListener.java
@@ -0,0 +1,78 @@
+package software.xdev.saveactions.core.listener;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.fileEditor.FileDocumentManagerListener;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiFile;
+
+import software.xdev.saveactions.core.ExecutionMode;
+import software.xdev.saveactions.core.service.SaveActionsService;
+import software.xdev.saveactions.core.service.SaveActionsServiceManager;
+import software.xdev.saveactions.model.Action;
+
+
+/**
+ * FileDocumentManagerListener to catch save events. This listener is registered as ExtensionPoint.
+ */
+public final class SaveActionsDocumentManagerListener implements FileDocumentManagerListener
+{
+ private static final Logger LOGGER = Logger.getInstance(SaveActionsService.class);
+
+ private final Project project;
+ private PsiDocumentManager psiDocumentManager;
+
+ public SaveActionsDocumentManagerListener(final Project project)
+ {
+ this.project = project;
+ }
+
+ @Override
+ public void beforeAllDocumentsSaving()
+ {
+ LOGGER.debug(
+ "[+] Start SaveActionsDocumentManagerListener#beforeAllDocumentsSaving, " + this.project.getName());
+
+ final List unsavedDocuments = Arrays.asList(FileDocumentManager.getInstance().getUnsavedDocuments());
+ if(!unsavedDocuments.isEmpty())
+ {
+ LOGGER.debug(String.format(
+ "Locating psi files for %d documents: %s",
+ unsavedDocuments.size(),
+ unsavedDocuments));
+ this.beforeDocumentsSaving(unsavedDocuments);
+ }
+ LOGGER.debug("End SaveActionsDocumentManagerListener#beforeAllDocumentsSaving");
+ }
+
+ public void beforeDocumentsSaving(final List documents)
+ {
+ if(this.project.isDisposed())
+ {
+ return;
+ }
+ this.initPsiDocManager();
+ final Set psiFiles = documents.stream()
+ .map(this.psiDocumentManager::getPsiFile)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toSet());
+ SaveActionsServiceManager.getService()
+ .guardedProcessPsiFiles(this.project, psiFiles, Action.activate, ExecutionMode.saveAll);
+ }
+
+ private synchronized void initPsiDocManager()
+ {
+ if(this.psiDocumentManager == null)
+ {
+ this.psiDocumentManager = PsiDocumentManager.getInstance(this.project);
+ }
+ }
+}
diff --git a/src/main/java/software/xdev/saveactions/core/service/SaveActionsService.java b/src/main/java/software/xdev/saveactions/core/service/SaveActionsService.java
new file mode 100644
index 0000000..e151c78
--- /dev/null
+++ b/src/main/java/software/xdev/saveactions/core/service/SaveActionsService.java
@@ -0,0 +1,28 @@
+package software.xdev.saveactions.core.service;
+
+import java.util.List;
+import java.util.Set;
+
+import com.intellij.openapi.actionSystem.ex.QuickList;
+import com.intellij.openapi.components.Service;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiFile;
+
+import software.xdev.saveactions.core.ExecutionMode;
+import software.xdev.saveactions.model.Action;
+
+
+/**
+ * This interface is implemented by all SaveAction ApplicationServices. It is used to be able to override a concrete
+ * implementation. Also, it has to be annotated with {@link Service}.
+ */
+public interface SaveActionsService
+{
+ void guardedProcessPsiFiles(Project project, Set psiFiles, Action activation, ExecutionMode mode);
+
+ boolean isJavaAvailable();
+
+ boolean isCompilingAvailable();
+
+ List getQuickLists(Project project);
+}
diff --git a/src/main/java/software/xdev/saveactions/core/service/SaveActionsServiceManager.java b/src/main/java/software/xdev/saveactions/core/service/SaveActionsServiceManager.java
new file mode 100644
index 0000000..f1c2f04
--- /dev/null
+++ b/src/main/java/software/xdev/saveactions/core/service/SaveActionsServiceManager.java
@@ -0,0 +1,46 @@
+package software.xdev.saveactions.core.service;
+
+import com.intellij.openapi.application.ApplicationManager;
+
+import software.xdev.saveactions.core.service.impl.SaveActionsDefaultService;
+import software.xdev.saveactions.core.service.impl.SaveActionsJavaService;
+
+
+/**
+ * SaveActionsServiceManager is providing the concrete service implementation. All actions are handled by the
+ * {@link SaveActionsService} implementation.
+ *
+ * @see SaveActionsDefaultService
+ * @see SaveActionsJavaService
+ */
+public final class SaveActionsServiceManager
+{
+ static SaveActionsService instance;
+
+ public static SaveActionsService getService()
+ {
+ if(instance == null)
+ {
+ initService();
+ }
+ return instance;
+ }
+
+ private static synchronized void initService()
+ {
+ if(instance != null)
+ {
+ return;
+ }
+
+ instance = ApplicationManager.getApplication().getService(SaveActionsJavaService.class);
+ if(instance == null)
+ {
+ instance = ApplicationManager.getApplication().getService(SaveActionsDefaultService.class);
+ }
+ }
+
+ private SaveActionsServiceManager()
+ {
+ }
+}
diff --git a/src/main/java/software/xdev/saveactions/core/service/impl/AbstractSaveActionsService.java b/src/main/java/software/xdev/saveactions/core/service/impl/AbstractSaveActionsService.java
new file mode 100644
index 0000000..a05245e
--- /dev/null
+++ b/src/main/java/software/xdev/saveactions/core/service/impl/AbstractSaveActionsService.java
@@ -0,0 +1,191 @@
+package software.xdev.saveactions.core.service.impl;
+
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toMap;
+import static software.xdev.saveactions.model.StorageFactory.JAVA;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Stream;
+
+import org.jetbrains.annotations.NotNull;
+
+import com.intellij.openapi.actionSystem.ex.QuickList;
+import com.intellij.openapi.actionSystem.ex.QuickListsManager;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.progress.EmptyProgressIndicator;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.Task;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiFile;
+
+import software.xdev.saveactions.core.ExecutionMode;
+import software.xdev.saveactions.core.component.Engine;
+import software.xdev.saveactions.core.service.SaveActionsService;
+import software.xdev.saveactions.model.Action;
+import software.xdev.saveactions.model.Storage;
+import software.xdev.saveactions.model.StorageFactory;
+import software.xdev.saveactions.processors.Processor;
+
+
+/**
+ * Super class for all ApplicationServices. All actions are routed here. ApplicationServices are Singleton
+ * implementations by default.
+ *
+ * The main method is {@link #guardedProcessPsiFiles(Project, Set, Action, ExecutionMode)} and will delegate to
+ * {@link Engine#processPsiFilesIfNecessary(ProgressIndicator, boolean)} ()}.
+ * The method will check if the file needs to be processed and uses the processors to apply the modifications.
+ *
+ * The psi files are ide wide, that means they are shared between projects (and editor windows), so we need to check if
+ * the file is physically in that project before reformatting, or else the file is formatted twice and intellij will ask
+ * to confirm unlocking of non-project file in the other project, see {@link Engine} for more details.
+ */
+abstract class AbstractSaveActionsService implements SaveActionsService
+{
+ protected static final Logger LOGGER = Logger.getInstance(SaveActionsService.class);
+
+ private final List processors;
+ private final StorageFactory storageFactory;
+ private final boolean javaAvailable;
+ private final boolean compilingAvailable;
+
+ private final Map guardedProcessPsiFilesLocks =
+ Collections.synchronizedMap(new WeakHashMap<>());
+
+ protected AbstractSaveActionsService(final StorageFactory storageFactory)
+ {
+ LOGGER.info("Save Actions Service \"" + this.getClass().getSimpleName() + "\" initialized.");
+ this.storageFactory = storageFactory;
+ this.processors = new ArrayList<>();
+ this.javaAvailable = JAVA.equals(storageFactory);
+ this.compilingAvailable = this.initCompilingAvailable();
+ }
+
+ @Override
+ public void guardedProcessPsiFiles(
+ final Project project,
+ final Set psiFiles,
+ final Action activation,
+ final ExecutionMode mode)
+ {
+ if(ApplicationManager.getApplication().isDisposed())
+ {
+ LOGGER.info("Application is closing, stopping invocation");
+ return;
+ }
+
+ final Storage storage = this.storageFactory.getStorage(project);
+ final Engine engine = new Engine(
+ storage,
+ this.processors,
+ project,
+ psiFiles,
+ activation,
+ mode);
+
+ final boolean applyAsync = storage.getActions().contains(Action.processAsync);
+ if(applyAsync)
+ {
+ new Task.Backgroundable(project, "Applying Save Actions", true)
+ {
+ @Override
+ public void run(@NotNull final ProgressIndicator indicator)
+ {
+ AbstractSaveActionsService.this.processPsiFilesIfNecessaryWithLock(project, engine, indicator);
+ }
+ }.queue();
+ return;
+ }
+
+ this.processPsiFilesIfNecessaryWithLock(project, engine, null);
+ }
+
+ private void processPsiFilesIfNecessaryWithLock(
+ final Project project,
+ final Engine engine,
+ final ProgressIndicator indicator)
+ {
+ if(LOGGER.isTraceEnabled())
+ {
+ LOGGER.trace("Getting lock - " + project.getName());
+ }
+ final ReentrantLock lock =
+ this.guardedProcessPsiFilesLocks.computeIfAbsent(project, ignored -> new ReentrantLock());
+ lock.lock();
+ if(LOGGER.isTraceEnabled())
+ {
+ LOGGER.trace("Got lock - " + project.getName());
+ }
+ try
+ {
+ engine.processPsiFilesIfNecessary(
+ indicator != null ? indicator : new EmptyProgressIndicator(),
+ indicator != null);
+ }
+ finally
+ {
+ lock.unlock();
+ if(LOGGER.isTraceEnabled())
+ {
+ LOGGER.trace("Released lock - " + project.getName());
+ }
+ }
+ }
+
+ @Override
+ public boolean isJavaAvailable()
+ {
+ return this.javaAvailable;
+ }
+
+ @Override
+ public boolean isCompilingAvailable()
+ {
+ return this.compilingAvailable;
+ }
+
+ @Override
+ public List getQuickLists(final Project project)
+ {
+ final Map quickListsIds =
+ Arrays.stream(QuickListsManager.getInstance().getAllQuickLists())
+ .collect(toMap(QuickList::hashCode, identity()));
+
+ return Optional.ofNullable(this.storageFactory.getStorage(project))
+ .map(storage -> storage.getQuickLists().stream()
+ .map(Integer::valueOf)
+ .map(quickListsIds::get)
+ .filter(Objects::nonNull)
+ .toList())
+ .orElseGet(List::of);
+ }
+
+ protected SaveActionsService addProcessors(final Stream processors)
+ {
+ processors.forEach(this.processors::add);
+ this.processors.sort(new Processor.OrderComparator());
+ return this;
+ }
+
+ private boolean initCompilingAvailable()
+ {
+ try
+ {
+ Class.forName("com.intellij.openapi.compiler.CompilerManager");
+ return true;
+ }
+ catch(final Exception e)
+ {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/software/xdev/saveactions/core/service/impl/SaveActionsDefaultService.java b/src/main/java/software/xdev/saveactions/core/service/impl/SaveActionsDefaultService.java
new file mode 100644
index 0000000..7fbe7aa
--- /dev/null
+++ b/src/main/java/software/xdev/saveactions/core/service/impl/SaveActionsDefaultService.java
@@ -0,0 +1,29 @@
+package software.xdev.saveactions.core.service.impl;
+
+import static software.xdev.saveactions.model.StorageFactory.DEFAULT;
+
+import software.xdev.saveactions.processors.BuildProcessor;
+import software.xdev.saveactions.processors.GlobalProcessor;
+
+
+/**
+ * This ApplicationService implementation is used for all IDE flavors that are not handling JAVA.
+ *
+ * It is assigned as ExtensionPoint from inside plugin.xml. In terms of IDEs using Java this service is overridden by
+ * the extended JAVA based version {@link SaveActionsJavaService}. Hence, it will not be loaded for Intellij IDEA,
+ * Android Studio a.s.o.
+ *
+ * Services must be final classes as per definition. That is the reason to use an abstract class here.
+ *
+ *
+ * @see AbstractSaveActionsService
+ */
+public final class SaveActionsDefaultService extends AbstractSaveActionsService
+{
+ public SaveActionsDefaultService()
+ {
+ super(DEFAULT);
+ this.addProcessors(BuildProcessor.stream());
+ this.addProcessors(GlobalProcessor.stream());
+ }
+}
diff --git a/src/main/java/software/xdev/saveactions/core/service/impl/SaveActionsJavaService.java b/src/main/java/software/xdev/saveactions/core/service/impl/SaveActionsJavaService.java
new file mode 100644
index 0000000..600d3bb
--- /dev/null
+++ b/src/main/java/software/xdev/saveactions/core/service/impl/SaveActionsJavaService.java
@@ -0,0 +1,31 @@
+package software.xdev.saveactions.core.service.impl;
+
+import static software.xdev.saveactions.model.StorageFactory.JAVA;
+
+import software.xdev.saveactions.processors.BuildProcessor;
+import software.xdev.saveactions.processors.GlobalProcessor;
+import software.xdev.saveactions.processors.java.JavaProcessor;
+
+
+/**
+ * This ApplicationService implementation is used for all JAVA based IDE flavors.
+ *
+ * It is assigned as ExtensionPoint from inside plugin-java.xml and overrides the default implementation
+ * {@link SaveActionsDefaultService} which is not being loaded for Intellij IDEA, Android Studio a.s.o. Instead this
+ * implementation will be assigned. Thus, all processors have to be configured by this class as well.
+ *
+ * Services must be final classes as per definition. That is the reason to use an abstract class here.
+ *
+ *
+ * @see AbstractSaveActionsService
+ */
+public final class SaveActionsJavaService extends AbstractSaveActionsService
+{
+ public SaveActionsJavaService()
+ {
+ super(JAVA);
+ this.addProcessors(BuildProcessor.stream());
+ this.addProcessors(GlobalProcessor.stream());
+ this.addProcessors(JavaProcessor.stream());
+ }
+}
diff --git a/src/main/java/software/xdev/saveactions/model/Action.java b/src/main/java/software/xdev/saveactions/model/Action.java
new file mode 100644
index 0000000..f3cfd08
--- /dev/null
+++ b/src/main/java/software/xdev/saveactions/model/Action.java
@@ -0,0 +1,156 @@
+package software.xdev.saveactions.model;
+
+import static java.util.stream.Collectors.toSet;
+import static software.xdev.saveactions.model.ActionType.activation;
+import static software.xdev.saveactions.model.ActionType.build;
+import static software.xdev.saveactions.model.ActionType.global;
+import static software.xdev.saveactions.model.ActionType.java;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Stream;
+
+
+@SuppressWarnings("java:S115")
+public enum Action
+{
+ // Activation
+ activate("Activate save actions on save (before saving each file, performs the configured actions below)",
+ activation, true),
+
+ activateOnShortcut("Activate save actions on shortcut (default \"CTRL + SHIFT + S\")",
+ activation, false),
+
+ activateOnBatch("Activate save actions on batch (\"Code > Save Actions > Execute on multiple files\")",
+ activation, false),
+
+ noActionIfCompileErrors("No action if compile errors (applied per file)",
+ activation, false),
+
+ processAsync("Process files asynchronously "
+ + "(will result in less UI hangs but may break if a processor needs the UI)",
+ activation, false),
+
+ // Global
+ organizeImports("Optimize imports",
+ global, true),
+
+ reformat("Reformat file",
+ global, true),
+
+ reformatChangedCode("Reformat only changed code (only if VCS configured)",
+ global, false),
+
+ rearrange("Rearrange fields and methods "
+ + "(configured in \"File > Settings > Editor > Code Style > (...) > Arrangement\")",
+ global, false),
+
+ // Build
+ compile("[experimental] Compile files (using \"Build > Build Project\")",
+ build, false),
+
+ reload("[experimental] Reload files in running debugger (using \"Run > Reload Changed Classes\")",
+ build, false),
+
+ executeAction("[experimental] Execute an action (using quick lists at "
+ + "\"File > Settings > Appearance & Behavior > Quick Lists\")",
+ build, false),
+
+ // Java fixes
+ fieldCanBeFinal("Add final modifier to field",
+ java, false),
+
+ localCanBeFinal("Add final modifier to local variable or parameter",
+ java, false),
+
+ localCanBeFinalExceptImplicit("Add final modifier to local variable or parameter except if it is implicit",
+ java, false),
+
+ methodMayBeStatic("Add static modifier to methods",
+ java, false),
+
+ unqualifiedFieldAccess("Add this to field access",
+ java, false),
+
+ unqualifiedMethodAccess("Add this to method access",
+ java, false),
+
+ unqualifiedStaticMemberAccess("Add class qualifier to static member access",
+ java, false),
+
+ customUnqualifiedStaticMemberAccess("Add class qualifier to static member access outside declaring class",
+ java, false),
+
+ missingOverrideAnnotation("Add missing @Override annotations",
+ java, false),
+
+ useBlocks("Add blocks to if/while/for statements",
+ java, false),
+
+ generateSerialVersionUID("Add a serialVersionUID field for Serializable classes",
+ java, false),
+
+ unnecessaryThis("Remove unnecessary this to field and method",
+ java, false),
+
+ finalPrivateMethod("Remove final from private method",
+ java, false),
+
+ unnecessaryFinalOnLocalVariableOrParameter("Remove unnecessary final for local variable or parameter",
+ java, false),
+
+ explicitTypeCanBeDiamond("Remove explicit generic type for diamond",
+ java, false),
+
+ unnecessarySemicolon("Remove unnecessary semicolon",
+ java, false),
+
+ singleStatementInBlock("Remove blocks from if/while/for statements",
+ java, false),
+
+ accessCanBeTightened("Change visibility of field or method to lower access",
+ java, false);
+
+ private final String text;
+ private final ActionType type;
+ private final boolean defaultValue;
+
+ Action(final String text, final ActionType type, final boolean defaultValue)
+ {
+ this.text = text;
+ this.type = type;
+ this.defaultValue = defaultValue;
+ }
+
+ public String getText()
+ {
+ return this.text;
+ }
+
+ public ActionType getType()
+ {
+ return this.type;
+ }
+
+ public boolean isDefaultValue()
+ {
+ return this.defaultValue;
+ }
+
+ public static Set getDefaults()
+ {
+ return Arrays.stream(Action.values())
+ .filter(Action::isDefaultValue)
+ .collect(toSet());
+ }
+
+ public static Stream stream()
+ {
+ return Arrays.stream(values());
+ }
+
+ public static Stream stream(final ActionType type)
+ {
+ return Arrays.stream(values()).filter(action -> action.type.equals(type));
+ }
+}
diff --git a/src/main/java/software/xdev/saveactions/model/ActionType.java b/src/main/java/software/xdev/saveactions/model/ActionType.java
new file mode 100644
index 0000000..1ec5357
--- /dev/null
+++ b/src/main/java/software/xdev/saveactions/model/ActionType.java
@@ -0,0 +1,13 @@
+package software.xdev.saveactions.model;
+
+public enum ActionType
+{
+
+ activation,
+
+ global,
+
+ build,
+
+ java
+}
diff --git a/src/main/java/software/xdev/saveactions/model/Storage.java b/src/main/java/software/xdev/saveactions/model/Storage.java
new file mode 100644
index 0000000..6efdca5
--- /dev/null
+++ b/src/main/java/software/xdev/saveactions/model/Storage.java
@@ -0,0 +1,156 @@
+package software.xdev.saveactions.model;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import org.jetbrains.annotations.NotNull;
+
+import com.intellij.openapi.components.PersistentStateComponent;
+import com.intellij.openapi.components.Service;
+import com.intellij.openapi.components.State;
+import com.intellij.serviceContainer.NonInjectable;
+import com.intellij.util.xmlb.XmlSerializerUtil;
+
+
+@State(name = "SaveActionSettings", storages = {@com.intellij.openapi.components.Storage("saveactions_settings.xml")})
+@Service(Service.Level.PROJECT)
+public final class Storage implements PersistentStateComponent
+{
+ private boolean firstLaunch;
+ private Set actions;
+ private Set exclusions;
+ private Set inclusions;
+ private String configurationPath;
+ private List quickLists;
+
+ @NonInjectable
+ public Storage()
+ {
+ this.firstLaunch = true;
+ this.actions = new HashSet<>();
+ this.exclusions = new HashSet<>();
+ this.inclusions = new HashSet<>();
+ this.configurationPath = null;
+ this.quickLists = new ArrayList<>();
+ }
+
+ @NonInjectable
+ public Storage(final Storage storage)
+ {
+ this.firstLaunch = storage.firstLaunch;
+ this.actions = new HashSet<>(storage.actions);
+ this.exclusions = new HashSet<>(storage.exclusions);
+ this.inclusions = new HashSet<>(storage.inclusions);
+ this.configurationPath = storage.configurationPath;
+ this.quickLists = new ArrayList<>(storage.quickLists);
+ }
+
+ @Override
+ public Storage getState()
+ {
+ return this;
+ }
+
+ @Override
+ public void loadState(@NotNull final Storage state)
+ {
+ this.firstLaunch = false;
+ XmlSerializerUtil.copyBean(state, this);
+
+ // Remove null values that might have been caused by non-parsable values
+ this.actions.removeIf(Objects::isNull);
+ this.exclusions.removeIf(Objects::isNull);
+ this.inclusions.removeIf(Objects::isNull);
+ this.quickLists.removeIf(Objects::isNull);
+ }
+
+ public Set getActions()
+ {
+ return this.actions;
+ }
+
+ public void setActions(final Set actions)
+ {
+ this.actions = actions;
+ }
+
+ public Set getExclusions()
+ {
+ return this.exclusions;
+ }
+
+ public void setExclusions(final Set exclusions)
+ {
+ this.exclusions = exclusions;
+ }
+
+ public boolean isEnabled(final Action action)
+ {
+ return this.actions.contains(action);
+ }
+
+ public void setEnabled(final Action action, final boolean enable)
+ {
+ if(enable)
+ {
+ this.actions.add(action);
+ }
+ else
+ {
+ this.actions.remove(action);
+ }
+ }
+
+ public Set getInclusions()
+ {
+ return this.inclusions;
+ }
+
+ public void setInclusions(final Set inclusions)
+ {
+ this.inclusions = inclusions;
+ }
+
+ public boolean isFirstLaunch()
+ {
+ return this.firstLaunch;
+ }
+
+ public void stopFirstLaunch()
+ {
+ this.firstLaunch = false;
+ }
+
+ public String getConfigurationPath()
+ {
+ return this.configurationPath;
+ }
+
+ public void setConfigurationPath(final String configurationPath)
+ {
+ this.configurationPath = configurationPath;
+ }
+
+ public List getQuickLists()
+ {
+ return this.quickLists;
+ }
+
+ public void setQuickLists(final List quickLists)
+ {
+ this.quickLists = quickLists;
+ }
+
+ public void clear()
+ {
+ this.firstLaunch = true;
+ this.actions.clear();
+ this.exclusions.clear();
+ this.inclusions.clear();
+ this.configurationPath = null;
+ this.quickLists.clear();
+ }
+}
diff --git a/src/main/java/software/xdev/saveactions/model/StorageFactory.java b/src/main/java/software/xdev/saveactions/model/StorageFactory.java
new file mode 100644
index 0000000..0ce6f04
--- /dev/null
+++ b/src/main/java/software/xdev/saveactions/model/StorageFactory.java
@@ -0,0 +1,30 @@
+package software.xdev.saveactions.model;
+
+import java.util.function.Function;
+
+import com.intellij.openapi.project.Project;
+
+import software.xdev.saveactions.model.java.EpfStorage;
+
+
+public enum StorageFactory
+{
+ DEFAULT(project -> project.getService(Storage.class)),
+
+ JAVA(project -> {
+ Storage defaultStorage = DEFAULT.getStorage(project);
+ return EpfStorage.INSTANCE.getStorageOrDefault(defaultStorage.getConfigurationPath(), defaultStorage);
+ });
+
+ private final Function provider;
+
+ StorageFactory(final Function provider)
+ {
+ this.provider = provider;
+ }
+
+ public Storage getStorage(final Project project)
+ {
+ return this.provider.apply(project);
+ }
+}
diff --git a/src/main/java/software/xdev/saveactions/model/java/EpfAction.java b/src/main/java/software/xdev/saveactions/model/java/EpfAction.java
new file mode 100644
index 0000000..8ea17b9
--- /dev/null
+++ b/src/main/java/software/xdev/saveactions/model/java/EpfAction.java
@@ -0,0 +1,95 @@
+package software.xdev.saveactions.model.java;
+
+import static java.util.Collections.unmodifiableList;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import software.xdev.saveactions.model.Action;
+
+
+public enum EpfAction
+{
+ organizeImports(
+ Action.organizeImports,
+ EpfKey.organize_imports, EpfKey.remove_unused_imports),
+
+ reformat(
+ Action.reformat,
+ EpfKey.format_source_code),
+
+ reformatChangedCode(
+ Action.reformatChangedCode,
+ EpfKey.format_source_code_changes_only),
+
+ rearrange(
+ Action.rearrange,
+ EpfKey.sort_members, EpfKey.sort_members_all),
+
+ fieldCanBeFinal(
+ Action.fieldCanBeFinal,
+ EpfKey.make_private_fields_final),
+
+ localCanBeFinal(
+ Action.localCanBeFinal,
+ EpfKey.make_local_variable_final),
+
+ unqualifiedFieldAccess(
+ Action.unqualifiedFieldAccess,
+ EpfKey.use_this_for_non_static_field_access),
+
+ unqualifiedMethodAccess(
+ Action.unqualifiedMethodAccess,
+ EpfKey.always_use_this_for_non_static_method_access),
+
+ unqualifiedStaticMemberAccess(
+ Action.unqualifiedStaticMemberAccess,
+ EpfKey.qualify_static_member_accesses_with_declaring_class),
+
+ missingOverrideAnnotation(
+ Action.missingOverrideAnnotation,
+ EpfKey.add_missing_override_annotations, EpfKey.add_missing_override_annotations_interface_methods),
+
+ useBlocks(
+ Action.useBlocks,
+ EpfKey.use_blocks, EpfKey.always_use_blocks),
+
+ generateSerialVersionUID(
+ Action.generateSerialVersionUID,
+ EpfKey.add_serial_version_id, EpfKey.add_default_serial_version_id, EpfKey.add_generated_serial_version_id),
+
+ explicitTypeCanBeDiamond(
+ Action.explicitTypeCanBeDiamond,
+ EpfKey.remove_redundant_type_arguments);
+
+ private final Action action;
+ private final List epfKeys;
+
+ EpfAction(final Action action, final EpfKey... epfKeys)
+ {
+ this.action = action;
+ this.epfKeys = Arrays.asList(epfKeys);
+ }
+
+ public Action getAction()
+ {
+ return this.action;
+ }
+
+ public List getEpfKeys()
+ {
+ return unmodifiableList(this.epfKeys);
+ }
+
+ public static Optional getEpfActionForAction(final Action action)
+ {
+ return stream().filter(epfAction -> epfAction.action.equals(action)).findFirst();
+ }
+
+ public static Stream stream()
+ {
+ return Arrays.stream(values());
+ }
+}
diff --git a/src/main/java/software/xdev/saveactions/model/java/EpfKey.java b/src/main/java/software/xdev/saveactions/model/java/EpfKey.java
new file mode 100644
index 0000000..a58d6c7
--- /dev/null
+++ b/src/main/java/software/xdev/saveactions/model/java/EpfKey.java
@@ -0,0 +1,84 @@
+package software.xdev.saveactions.model.java;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Stream;
+
+
+@SuppressWarnings("java:S115")
+public enum EpfKey
+{
+ add_default_serial_version_id,
+ add_generated_serial_version_id,
+ add_missing_annotations,
+ add_missing_deprecated_annotations,
+ add_missing_methods,
+ add_missing_nls_tags,
+ add_missing_override_annotations,
+ add_missing_override_annotations_interface_methods,
+ add_serial_version_id,
+ always_use_blocks,
+ always_use_parentheses_in_expressions,
+ always_use_this_for_non_static_field_access,
+ always_use_this_for_non_static_method_access,
+ convert_functional_interfaces,
+ convert_to_enhanced_for_loop,
+ correct_indentation,
+ format_source_code,
+ format_source_code_changes_only,
+ insert_inferred_type_arguments,
+ make_local_variable_final,
+ make_parameters_final,
+ make_private_fields_final,
+ make_type_abstract_if_missing_method,
+ make_variable_declarations_final,
+ never_use_blocks,
+ never_use_parentheses_in_expressions,
+ on_save_use_additional_actions,
+ organize_imports,
+ qualify_static_field_accesses_with_declaring_class,
+ qualify_static_member_accesses_through_instances_with_declaring_class,
+ qualify_static_member_accesses_through_subtypes_with_declaring_class,
+ qualify_static_member_accesses_with_declaring_class,
+ qualify_static_method_accesses_with_declaring_class,
+ remove_private_constructors,
+ remove_redundant_type_arguments,
+ remove_trailing_whitespaces,
+ remove_trailing_whitespaces_all,
+ remove_trailing_whitespaces_ignore_empty,
+ remove_unnecessary_casts,
+ remove_unnecessary_nls_tags,
+ remove_unused_imports,
+ remove_unused_local_variables,
+ remove_unused_private_fields,
+ remove_unused_private_members,
+ remove_unused_private_methods,
+ remove_unused_private_types,
+ sort_members,
+ sort_members_all,
+ use_anonymous_class_creation,
+ use_blocks,
+ use_blocks_only_for_return_and_throw,
+ use_lambda,
+ use_parentheses_in_expressions,
+ use_this_for_non_static_field_access,
+ use_this_for_non_static_field_access_only_if_necessary,
+ use_this_for_non_static_method_access,
+ use_this_for_non_static_method_access_only_if_necessary;
+
+ private static final List PREFIXES = Arrays.asList(
+ "sp_cleanup",
+ "/instance/org.eclipse.jdt.ui/sp_cleanup"
+ );
+
+ public static List getPrefixes()
+ {
+ return Collections.unmodifiableList(PREFIXES);
+ }
+
+ public static Stream stream()
+ {
+ return Arrays.stream(values());
+ }
+}
diff --git a/src/main/java/software/xdev/saveactions/model/java/EpfStorage.java b/src/main/java/software/xdev/saveactions/model/java/EpfStorage.java
new file mode 100644
index 0000000..63e5957
--- /dev/null
+++ b/src/main/java/software/xdev/saveactions/model/java/EpfStorage.java
@@ -0,0 +1,96 @@
+package software.xdev.saveactions.model.java;
+
+import static java.util.Collections.emptyList;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import java.util.Properties;
+
+import com.intellij.openapi.diagnostic.Logger;
+
+import software.xdev.saveactions.core.service.SaveActionsService;
+import software.xdev.saveactions.model.Action;
+import software.xdev.saveactions.model.Storage;
+
+
+/**
+ * Storage implementation for the Workspace-Mechanic-Format. Only the Java language-specific actions are supported.
+ *
+ * The main method {@link #getStorageOrDefault(String, Storage)} return a configuration based on EPF if the path to EPF
+ * configuration file is set and valid, or else the default configuration is returned.
+ *
When this list is empty, all files are included. When you add inclusion expressions, only the files "
+ + "that match will be impacted by the save actions.
"
+ + "
(use case sensitive Java regular expression that matches the end of the full file path)
"
+ + "
"
+ + "
.*\\.java (include all '.java' in all folders)
"
+ + "
Include\\.java (include file 'Include.java' in all folders)
"
+ + "
src/Include\\.java (include file 'Include.java' in 'src' folders)
"
+ + "
src/.* (include folder 'src' recursively)
"
+ + "
myProject/Include.md (include file 'Include.md' in project 'myProject')
Supports configurable, Eclipse like, save actions, including "optimize imports", "reformat code", "rearrange code", "compile file" and some quick fixes for Java like "add / remove 'this' qualifier", etc. The plugin executes the configured actions when the file is synchronised (or saved) on disk.