Skip to content

Commit c81918d

Browse files
authored
Merge pull request #154 from xdev-software/develop
Release
2 parents 40bac66 + 47f2450 commit c81918d

File tree

9 files changed

+233
-25
lines changed

9 files changed

+233
-25
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## 1.3.0
2+
* Make it possible to run processors asynchronously #130
3+
* This way the UI should be more responsive when processing a lot of files
4+
* May break processors that interact with the UI e.g. when showing dialogs
5+
* Don't process files during project load #145
6+
* This should cause less race conditions due to partial project initialization
7+
* Only active on IntelliJ < 2024.3 as [the underlying problem was fixed in IntelliJ 2024.3](https://github.com/JetBrains/intellij-community/commit/765caa71175d0a67a54836cf840fae829da590d9)
8+
19
## 1.2.4
210
* Dropped support for IntelliJ versions < 2024.2
311
* Removed deprecated code that was only required for older IDE versions

USAGE.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ You can quickly toggle the plugin activation by using the "Enable Save Action" a
3232
| 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")
3333
| Activate save actions on batch | Enable / disable the plugin on batch, by using "Code > Save Actions > Execute on multiple files"
3434
| No action if compile errors | Enable / disable no action if there are compile errors. Applied to each file individually
35+
| 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.
3536

3637
### Global
3738

@@ -132,3 +133,21 @@ Some things to note when using other plugins with the Save Actions plugin:
132133

133134
- **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`.
134135
- **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`
136+
137+
## Troubleshooting of common problems
138+
139+
### "Conflicting component name 'SaveActionSettings': class ``com.dubreuia.model.Storage`` and class ``software.xdev.saveactions.model.Storage``"
140+
141+
The problem only happens when the [old/deprecated/forked plugin](https://github.com/dubreuia/intellij-plugin-save-actions) plugin is also installed.
142+
143+
You can fix this by uninstalling the deprecated plugin.
144+
145+
### "AWT events are not allowed inside write action" occurs when applying Save Actions
146+
147+
This usually indicates that some action causes a UI dialog to show up.<br/>However as the actions are run in the background the dialog can't be shown and the crash occurs.
148+
149+
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.
150+
151+
### "Execute Save Actions on multiple files" is not working
152+
153+
Make sure that you enabled "Activate save actions on batch" in the settings.

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
pluginGroup=software.xdev.saveactions
33
pluginName=Save Actions X
44
# SemVer format -> https://semver.org
5-
pluginVersion=1.2.5-SNAPSHOT
5+
pluginVersion=1.3.0-SNAPSHOT
66
# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
77
platformType=IC
88
platformVersion=2024.2.2

src/main/java/software/xdev/saveactions/core/component/Engine.java

Lines changed: 103 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,27 @@
66
import java.util.List;
77
import java.util.Objects;
88
import java.util.Set;
9+
import java.util.concurrent.atomic.AtomicInteger;
910
import java.util.regex.Matcher;
1011
import java.util.regex.Pattern;
1112
import java.util.regex.PatternSyntaxException;
1213

1314
import org.jetbrains.annotations.NotNull;
1415

16+
import com.intellij.openapi.application.ReadAction;
17+
import com.intellij.openapi.application.WriteAction;
1518
import com.intellij.openapi.diagnostic.Logger;
1619
import com.intellij.openapi.editor.Document;
1720
import com.intellij.openapi.fileEditor.FileDocumentManager;
21+
import com.intellij.openapi.progress.ProgressIndicator;
1822
import com.intellij.openapi.project.Project;
1923
import com.intellij.openapi.roots.ProjectRootManager;
24+
import com.intellij.openapi.util.ThrowableComputable;
2025
import com.intellij.psi.PsiDocumentManager;
2126
import com.intellij.psi.PsiFile;
27+
import com.intellij.util.ApplicationKt;
2228
import com.intellij.util.PsiErrorElementUtil;
29+
import com.intellij.util.ThrowableRunnable;
2330

2431
import software.xdev.saveactions.core.ExecutionMode;
2532
import software.xdev.saveactions.core.service.SaveActionsService;
@@ -62,7 +69,9 @@ public Engine(
6269
this.mode = mode;
6370
}
6471

65-
public void processPsiFilesIfNecessary()
72+
public void processPsiFilesIfNecessary(
73+
@NotNull final ProgressIndicator indicator,
74+
final boolean async)
6675
{
6776
if(this.psiFiles == null)
6877
{
@@ -73,36 +82,112 @@ public void processPsiFilesIfNecessary()
7382
LOGGER.info(String.format("Action \"%s\" not enabled on %s", this.activation.getText(), this.project));
7483
return;
7584
}
85+
86+
indicator.setIndeterminate(true);
87+
final Set<PsiFile> psiFilesEligible = this.getEligiblePsiFiles(indicator, async);
88+
if(psiFilesEligible.isEmpty())
89+
{
90+
LOGGER.info("No files are eligible");
91+
return;
92+
}
93+
94+
final List<SaveCommand> processorsEligible = this.getEligibleProcessors(indicator, psiFilesEligible);
95+
if(processorsEligible.isEmpty())
96+
{
97+
LOGGER.info("No processors are eligible");
98+
return;
99+
}
100+
101+
this.flushPsiFiles(indicator, async, psiFilesEligible);
102+
103+
this.execute(indicator, processorsEligible, psiFilesEligible);
104+
}
105+
106+
private Set<PsiFile> getEligiblePsiFiles(final @NotNull ProgressIndicator indicator, final boolean async)
107+
{
76108
LOGGER.info(String.format("Processing %s files %s mode %s", this.project, this.psiFiles, this.mode));
77-
final Set<PsiFile> psiFilesEligible = this.psiFiles.stream()
78-
.filter(psiFile -> this.isPsiFileEligible(this.project, psiFile))
79-
.collect(toSet());
109+
indicator.checkCanceled();
110+
indicator.setText2("Collecting files to process");
111+
112+
final ThrowableComputable<Set<PsiFile>, RuntimeException> psiFilesEligibleFunc =
113+
() -> this.psiFiles.stream()
114+
.filter(psiFile -> this.isPsiFileEligible(this.project, psiFile))
115+
.collect(toSet());
116+
final Set<PsiFile> psiFilesEligible = async
117+
? ReadAction.compute(psiFilesEligibleFunc)
118+
: psiFilesEligibleFunc.compute();
80119
LOGGER.info(String.format("Valid files %s", psiFilesEligible));
81-
this.processPsiFiles(this.project, psiFilesEligible, this.mode);
120+
return psiFilesEligible;
82121
}
83122

84-
private void processPsiFiles(final Project project, final Set<PsiFile> psiFiles, final ExecutionMode mode)
123+
private @NotNull List<SaveCommand> getEligibleProcessors(
124+
final @NotNull ProgressIndicator indicator,
125+
final Set<PsiFile> psiFilesEligible)
85126
{
86-
if(psiFiles.isEmpty())
87-
{
88-
return;
89-
}
90127
LOGGER.info(String.format("Start processors (%d)", this.processors.size()));
128+
indicator.checkCanceled();
129+
indicator.setText2("Collecting processors");
130+
91131
final List<SaveCommand> processorsEligible = this.processors.stream()
92-
.map(processor -> processor.getSaveCommand(project, psiFiles))
132+
.map(processor -> processor.getSaveCommand(this.project, psiFilesEligible))
93133
.filter(command -> this.storage.isEnabled(command.getAction()))
94-
.filter(command -> command.getModes().contains(mode))
134+
.filter(command -> command.getModes().contains(this.mode))
95135
.toList();
96136
LOGGER.info(String.format("Filtered processors %s", processorsEligible));
97-
if(!processorsEligible.isEmpty())
137+
return processorsEligible;
138+
}
139+
140+
private void flushPsiFiles(
141+
final @NotNull ProgressIndicator indicator,
142+
final boolean async,
143+
final Set<PsiFile> psiFilesEligible)
144+
{
145+
LOGGER.info(String.format("Flushing files (%d)", psiFilesEligible.size()));
146+
indicator.checkCanceled();
147+
indicator.setText2("Flushing files");
148+
149+
final ThrowableRunnable<RuntimeException> flushFilesFunc = () -> {
150+
final PsiDocumentManager psiDocumentManager = PsiDocumentManager.getInstance(this.project);
151+
psiFilesEligible.forEach(psiFile -> this.commitDocumentAndSave(psiFile, psiDocumentManager));
152+
};
153+
if(async)
154+
{
155+
ApplicationKt.getApplication().invokeAndWait(() -> WriteAction.run(flushFilesFunc));
156+
}
157+
else
98158
{
99-
final PsiDocumentManager psiDocumentManager = PsiDocumentManager.getInstance(project);
100-
psiFiles.forEach(psiFile -> this.commitDocumentAndSave(psiFile, psiDocumentManager));
159+
flushFilesFunc.run();
101160
}
102-
final List<SimpleEntry<Action, Result<ResultCode>>> results = processorsEligible.stream()
161+
}
162+
163+
private void execute(
164+
final @NotNull ProgressIndicator indicator,
165+
final List<SaveCommand> processorsEligible,
166+
final Set<PsiFile> psiFilesEligible)
167+
{
168+
indicator.checkCanceled();
169+
indicator.setIndeterminate(false);
170+
indicator.setFraction(0d);
171+
172+
final List<SaveCommand> saveCommands = processorsEligible.stream()
103173
.filter(Objects::nonNull)
104-
.peek(command -> LOGGER.info(String.format("Execute command %s on %d files", command, psiFiles.size())))
105-
.map(command -> new SimpleEntry<>(command.getAction(), command.execute()))
174+
.toList();
175+
176+
final AtomicInteger executedCount = new AtomicInteger();
177+
final List<SimpleEntry<Action, Result<ResultCode>>> results = saveCommands.stream()
178+
.map(command -> {
179+
LOGGER.info(String.format("Execute command %s on %d files", command, psiFilesEligible.size()));
180+
181+
indicator.checkCanceled();
182+
indicator.setText2("Executing '" + command.getAction().getText() + "'");
183+
184+
final SimpleEntry<Action, Result<ResultCode>> entry =
185+
new SimpleEntry<>(command.getAction(), command.execute());
186+
187+
indicator.setFraction((double)executedCount.incrementAndGet() / saveCommands.size());
188+
189+
return entry;
190+
})
106191
.toList();
107192
LOGGER.info(String.format("Exit engine with results %s", results.stream()
108193
.map(entry -> entry.getKey() + ":" + entry.getValue())

src/main/java/software/xdev/saveactions/core/listener/SaveActionsDocumentManagerListener.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import java.util.Objects;
66
import java.util.Set;
77
import java.util.stream.Collectors;
8+
import java.util.stream.Stream;
89

10+
import com.intellij.openapi.application.ApplicationInfo;
911
import com.intellij.openapi.diagnostic.Logger;
1012
import com.intellij.openapi.editor.Document;
1113
import com.intellij.openapi.fileEditor.FileDocumentManager;
@@ -40,6 +42,13 @@ public void beforeAllDocumentsSaving()
4042
{
4143
LOGGER.debug(
4244
"[+] Start SaveActionsDocumentManagerListener#beforeAllDocumentsSaving, " + this.project.getName());
45+
46+
if(REQUIRES_PROJECT_LOAD_IGNORE_WORKAROUND && isInvokedFromProjectLoadBefore243())
47+
{
48+
LOGGER.debug("Ignoring due to PROJECT_LOAD_IGNORE_WORKAROUND");
49+
return;
50+
}
51+
4352
final List<Document> unsavedDocuments = Arrays.asList(FileDocumentManager.getInstance().getUnsavedDocuments());
4453
if(!unsavedDocuments.isEmpty())
4554
{
@@ -74,4 +83,38 @@ private synchronized void initPsiDocManager()
7483
this.psiDocumentManager = PsiDocumentManager.getInstance(this.project);
7584
}
7685
}
86+
87+
// region PROJECT_LOAD_IGNORE_WORKAROUND
88+
// https://github.com/xdev-software/intellij-plugin-save-actions/issues/145
89+
private static final boolean REQUIRES_PROJECT_LOAD_IGNORE_WORKAROUND =
90+
determineIfRequiresProjectLoadIgnoreWorkaround();
91+
92+
@SuppressWarnings("checkstyle:MagicNumber")
93+
static boolean determineIfRequiresProjectLoadIgnoreWorkaround()
94+
{
95+
try
96+
{
97+
// Problem was fixed in 2024.3
98+
return ApplicationInfo.getInstance().getBuild().getBaselineVersion() < 243;
99+
}
100+
catch(final Exception ex)
101+
{
102+
LOGGER.warn("Failed to determine IDE version", ex);
103+
return false;
104+
}
105+
}
106+
107+
@SuppressWarnings("checkstyle:MagicNumber")
108+
static boolean isInvokedFromProjectLoadBefore243()
109+
{
110+
// The invoking method ProjectSettingsTracker$submitSettingsFilesRefresh is usually at index 17 and 18
111+
return Stream.of(Thread.currentThread().getStackTrace())
112+
.map(StackTraceElement::getClassName)
113+
.skip(16)
114+
.limit(5)
115+
.anyMatch(s -> s.startsWith(
116+
"com.intellij.openapi.externalSystem.autoimport.ProjectSettingsTracker$submitSettingsFilesRefresh"));
117+
}
118+
119+
// endregion
77120
}

src/main/java/software/xdev/saveactions/core/service/SaveActionsService.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
*/
1919
public interface SaveActionsService
2020
{
21-
2221
void guardedProcessPsiFiles(Project project, Set<PsiFile> psiFiles, Action activation, ExecutionMode mode);
2322

2423
boolean isJavaAvailable();

src/main/java/software/xdev/saveactions/core/service/impl/AbstractSaveActionsService.java

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,26 @@
1212
import java.util.Objects;
1313
import java.util.Optional;
1414
import java.util.Set;
15+
import java.util.concurrent.locks.ReentrantLock;
1516
import java.util.stream.Stream;
1617

18+
import org.jetbrains.annotations.NotNull;
19+
1720
import com.intellij.openapi.actionSystem.ex.QuickList;
1821
import com.intellij.openapi.actionSystem.ex.QuickListsManager;
1922
import com.intellij.openapi.application.ApplicationManager;
2023
import com.intellij.openapi.diagnostic.Logger;
24+
import com.intellij.openapi.progress.EmptyProgressIndicator;
25+
import com.intellij.openapi.progress.ProgressIndicator;
26+
import com.intellij.openapi.progress.Task;
2127
import com.intellij.openapi.project.Project;
2228
import com.intellij.psi.PsiFile;
2329

2430
import software.xdev.saveactions.core.ExecutionMode;
2531
import software.xdev.saveactions.core.component.Engine;
2632
import software.xdev.saveactions.core.service.SaveActionsService;
2733
import software.xdev.saveactions.model.Action;
34+
import software.xdev.saveactions.model.Storage;
2835
import software.xdev.saveactions.model.StorageFactory;
2936
import software.xdev.saveactions.processors.Processor;
3037

@@ -34,8 +41,8 @@
3441
* implementations by default.
3542
* <p>
3643
* The main method is {@link #guardedProcessPsiFiles(Project, Set, Action, ExecutionMode)} and will delegate to
37-
* {@link Engine#processPsiFilesIfNecessary()}. The method will check if the file needs to be processed and uses the
38-
* processors to apply the modifications.
44+
* {@link Engine#processPsiFilesIfNecessary(ProgressIndicator, boolean)} ()}.
45+
* The method will check if the file needs to be processed and uses the processors to apply the modifications.
3946
* <p>
4047
* The psi files are ide wide, that means they are shared between projects (and editor windows), so we need to check if
4148
* the file is physically in that project before reformatting, or else the file is formatted twice and intellij will ask
@@ -52,6 +59,8 @@ abstract class AbstractSaveActionsService implements SaveActionsService
5259
private final boolean javaAvailable;
5360
private final boolean compilingAvailable;
5461

62+
private final ReentrantLock guardedProcessPsiFilesLock = new ReentrantLock();
63+
5564
protected AbstractSaveActionsService(final StorageFactory storageFactory)
5665
{
5766
LOGGER.info("Save Actions Service \"" + this.getClass().getSimpleName() + "\" initialized.");
@@ -62,7 +71,7 @@ protected AbstractSaveActionsService(final StorageFactory storageFactory)
6271
}
6372

6473
@Override
65-
public synchronized void guardedProcessPsiFiles(
74+
public void guardedProcessPsiFiles(
6675
final Project project,
6776
final Set<PsiFile> psiFiles,
6877
final Action activation,
@@ -73,10 +82,49 @@ public synchronized void guardedProcessPsiFiles(
7382
LOGGER.info("Application is closing, stopping invocation");
7483
return;
7584
}
85+
86+
final Storage storage = this.storageFactory.getStorage(project);
7687
final Engine engine = new Engine(
77-
this.storageFactory.getStorage(project), this.processors, project, psiFiles, activation,
88+
storage,
89+
this.processors,
90+
project,
91+
psiFiles,
92+
activation,
7893
mode);
79-
engine.processPsiFilesIfNecessary();
94+
95+
final boolean applyAsync = storage.getActions().contains(Action.processAsync);
96+
if(applyAsync)
97+
{
98+
new Task.Backgroundable(project, "Applying Save Actions", true)
99+
{
100+
@Override
101+
public void run(@NotNull final ProgressIndicator indicator)
102+
{
103+
AbstractSaveActionsService.this.processPsiFilesIfNecessaryWithLock(engine, indicator);
104+
}
105+
}.queue();
106+
return;
107+
}
108+
109+
this.processPsiFilesIfNecessaryWithLock(engine, null);
110+
}
111+
112+
private void processPsiFilesIfNecessaryWithLock(final Engine engine, final ProgressIndicator indicator)
113+
{
114+
LOGGER.trace("Getting lock");
115+
this.guardedProcessPsiFilesLock.lock();
116+
LOGGER.trace("Got lock");
117+
try
118+
{
119+
engine.processPsiFilesIfNecessary(
120+
indicator != null ? indicator : new EmptyProgressIndicator(),
121+
indicator != null);
122+
}
123+
finally
124+
{
125+
this.guardedProcessPsiFilesLock.unlock();
126+
LOGGER.trace("Released lock");
127+
}
80128
}
81129

82130
@Override

0 commit comments

Comments
 (0)