Skip to content

Commit 3bb59f0

Browse files
committed
Add basic support for async processing of files
Should improve #130
1 parent 929f3a9 commit 3bb59f0

File tree

5 files changed

+162
-24
lines changed

5 files changed

+162
-24
lines changed

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/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

src/main/java/software/xdev/saveactions/model/Action.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ public enum Action
2727
noActionIfCompileErrors("No action if compile errors (applied per file)",
2828
activation, false),
2929

30+
processAsync("Process files asynchronously "
31+
+ "(will result in less UI hangs but may break if a processor needs the UI)",
32+
activation, false),
33+
3034
// Global
3135
organizeImports("Optimize imports",
3236
global, true),

src/main/java/software/xdev/saveactions/ui/GeneralPanel.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static software.xdev.saveactions.model.Action.activateOnBatch;
55
import static software.xdev.saveactions.model.Action.activateOnShortcut;
66
import static software.xdev.saveactions.model.Action.noActionIfCompileErrors;
7+
import static software.xdev.saveactions.model.Action.processAsync;
78

89
import java.awt.Dimension;
910
import java.util.Map;
@@ -38,6 +39,7 @@ JPanel getPanel()
3839
panel.add(this.checkboxes.get(activateOnShortcut));
3940
panel.add(this.checkboxes.get(activateOnBatch));
4041
panel.add(this.checkboxes.get(noActionIfCompileErrors));
42+
panel.add(this.checkboxes.get(processAsync));
4143
panel.add(Box.createHorizontalGlue());
4244
panel.setMinimumSize(new Dimension(Short.MAX_VALUE, 0));
4345
return panel;

0 commit comments

Comments
 (0)