Skip to content

Commit

Permalink
Add basic support for async processing of files
Browse files Browse the repository at this point in the history
Should improve #130
  • Loading branch information
AB-xdev committed Sep 20, 2024
1 parent 929f3a9 commit 3bb59f0
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 24 deletions.
121 changes: 103 additions & 18 deletions src/main/java/software/xdev/saveactions/core/component/Engine.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,27 @@
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;
Expand Down Expand Up @@ -62,7 +69,9 @@ public Engine(
this.mode = mode;
}

public void processPsiFilesIfNecessary()
public void processPsiFilesIfNecessary(
@NotNull final ProgressIndicator indicator,
final boolean async)
{
if(this.psiFiles == null)
{
Expand All @@ -73,36 +82,112 @@ public void processPsiFilesIfNecessary()
LOGGER.info(String.format("Action \"%s\" not enabled on %s", this.activation.getText(), this.project));
return;
}

indicator.setIndeterminate(true);
final Set<PsiFile> psiFilesEligible = this.getEligiblePsiFiles(indicator, async);
if(psiFilesEligible.isEmpty())
{
LOGGER.info("No files are eligible");
return;
}

final List<SaveCommand> processorsEligible = this.getEligibleProcessors(indicator, psiFilesEligible);
if(processorsEligible.isEmpty())
{
LOGGER.info("No processors are eligible");
return;
}

this.flushPsiFiles(indicator, async, psiFilesEligible);

this.execute(indicator, processorsEligible, psiFilesEligible);
}

private Set<PsiFile> getEligiblePsiFiles(final @NotNull ProgressIndicator indicator, final boolean async)
{
LOGGER.info(String.format("Processing %s files %s mode %s", this.project, this.psiFiles, this.mode));
final Set<PsiFile> psiFilesEligible = this.psiFiles.stream()
.filter(psiFile -> this.isPsiFileEligible(this.project, psiFile))
.collect(toSet());
indicator.checkCanceled();
indicator.setText2("Collecting files to process");

final ThrowableComputable<Set<PsiFile>, RuntimeException> psiFilesEligibleFunc =
() -> this.psiFiles.stream()
.filter(psiFile -> this.isPsiFileEligible(this.project, psiFile))
.collect(toSet());
final Set<PsiFile> psiFilesEligible = async
? ReadAction.compute(psiFilesEligibleFunc)
: psiFilesEligibleFunc.compute();
LOGGER.info(String.format("Valid files %s", psiFilesEligible));
this.processPsiFiles(this.project, psiFilesEligible, this.mode);
return psiFilesEligible;
}

private void processPsiFiles(final Project project, final Set<PsiFile> psiFiles, final ExecutionMode mode)
private @NotNull List<SaveCommand> getEligibleProcessors(
final @NotNull ProgressIndicator indicator,
final Set<PsiFile> psiFilesEligible)
{
if(psiFiles.isEmpty())
{
return;
}
LOGGER.info(String.format("Start processors (%d)", this.processors.size()));
indicator.checkCanceled();
indicator.setText2("Collecting processors");

final List<SaveCommand> processorsEligible = this.processors.stream()
.map(processor -> processor.getSaveCommand(project, psiFiles))
.map(processor -> processor.getSaveCommand(this.project, psiFilesEligible))
.filter(command -> this.storage.isEnabled(command.getAction()))
.filter(command -> command.getModes().contains(mode))
.filter(command -> command.getModes().contains(this.mode))
.toList();
LOGGER.info(String.format("Filtered processors %s", processorsEligible));
if(!processorsEligible.isEmpty())
return processorsEligible;
}

private void flushPsiFiles(
final @NotNull ProgressIndicator indicator,
final boolean async,
final Set<PsiFile> psiFilesEligible)
{
LOGGER.info(String.format("Flushing files (%d)", psiFilesEligible.size()));
indicator.checkCanceled();
indicator.setText2("Flushing files");

final ThrowableRunnable<RuntimeException> flushFilesFunc = () -> {
final PsiDocumentManager psiDocumentManager = PsiDocumentManager.getInstance(this.project);
psiFilesEligible.forEach(psiFile -> this.commitDocumentAndSave(psiFile, psiDocumentManager));
};
if(async)
{
ApplicationKt.getApplication().invokeAndWait(() -> WriteAction.run(flushFilesFunc));
}
else
{
final PsiDocumentManager psiDocumentManager = PsiDocumentManager.getInstance(project);
psiFiles.forEach(psiFile -> this.commitDocumentAndSave(psiFile, psiDocumentManager));
flushFilesFunc.run();
}
final List<SimpleEntry<Action, Result<ResultCode>>> results = processorsEligible.stream()
}

private void execute(
final @NotNull ProgressIndicator indicator,
final List<SaveCommand> processorsEligible,
final Set<PsiFile> psiFilesEligible)
{
indicator.checkCanceled();
indicator.setIndeterminate(false);
indicator.setFraction(0d);

final List<SaveCommand> saveCommands = processorsEligible.stream()
.filter(Objects::nonNull)
.peek(command -> LOGGER.info(String.format("Execute command %s on %d files", command, psiFiles.size())))
.map(command -> new SimpleEntry<>(command.getAction(), command.execute()))
.toList();

final AtomicInteger executedCount = new AtomicInteger();
final List<SimpleEntry<Action, Result<ResultCode>>> results = saveCommands.stream()
.map(command -> {
LOGGER.info(String.format("Execute command %s on %d files", command, psiFilesEligible.size()));

indicator.checkCanceled();
indicator.setText2("Executing '" + command.getAction().getText() + "'");

final SimpleEntry<Action, Result<ResultCode>> 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", results.stream()
.map(entry -> entry.getKey() + ":" + entry.getValue())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
*/
public interface SaveActionsService
{

void guardedProcessPsiFiles(Project project, Set<PsiFile> psiFiles, Action activation, ExecutionMode mode);

boolean isJavaAvailable();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,26 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
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;

Expand All @@ -34,8 +41,8 @@
* implementations by default.
* <p>
* The main method is {@link #guardedProcessPsiFiles(Project, Set, Action, ExecutionMode)} and will delegate to
* {@link Engine#processPsiFilesIfNecessary()}. The method will check if the file needs to be processed and uses the
* processors to apply the modifications.
* {@link Engine#processPsiFilesIfNecessary(ProgressIndicator, boolean)} ()}.
* The method will check if the file needs to be processed and uses the processors to apply the modifications.
* <p>
* 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
Expand All @@ -52,6 +59,8 @@ abstract class AbstractSaveActionsService implements SaveActionsService
private final boolean javaAvailable;
private final boolean compilingAvailable;

private final ReentrantLock guardedProcessPsiFilesLock = new ReentrantLock();

protected AbstractSaveActionsService(final StorageFactory storageFactory)
{
LOGGER.info("Save Actions Service \"" + this.getClass().getSimpleName() + "\" initialized.");
Expand All @@ -62,7 +71,7 @@ protected AbstractSaveActionsService(final StorageFactory storageFactory)
}

@Override
public synchronized void guardedProcessPsiFiles(
public void guardedProcessPsiFiles(
final Project project,
final Set<PsiFile> psiFiles,
final Action activation,
Expand All @@ -73,10 +82,49 @@ public synchronized void guardedProcessPsiFiles(
LOGGER.info("Application is closing, stopping invocation");
return;
}

final Storage storage = this.storageFactory.getStorage(project);
final Engine engine = new Engine(
this.storageFactory.getStorage(project), this.processors, project, psiFiles, activation,
storage,
this.processors,
project,
psiFiles,
activation,
mode);
engine.processPsiFilesIfNecessary();

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(engine, indicator);
}
}.queue();
return;
}

this.processPsiFilesIfNecessaryWithLock(engine, null);
}

private void processPsiFilesIfNecessaryWithLock(final Engine engine, final ProgressIndicator indicator)
{
LOGGER.trace("Getting lock");
this.guardedProcessPsiFilesLock.lock();
LOGGER.trace("Got lock");
try
{
engine.processPsiFilesIfNecessary(
indicator != null ? indicator : new EmptyProgressIndicator(),
indicator != null);
}
finally
{
this.guardedProcessPsiFilesLock.unlock();
LOGGER.trace("Released lock");
}
}

@Override
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/software/xdev/saveactions/model/Action.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ public enum Action
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),
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/software/xdev/saveactions/ui/GeneralPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static software.xdev.saveactions.model.Action.activateOnBatch;
import static software.xdev.saveactions.model.Action.activateOnShortcut;
import static software.xdev.saveactions.model.Action.noActionIfCompileErrors;
import static software.xdev.saveactions.model.Action.processAsync;

import java.awt.Dimension;
import java.util.Map;
Expand Down Expand Up @@ -38,6 +39,7 @@ JPanel getPanel()
panel.add(this.checkboxes.get(activateOnShortcut));
panel.add(this.checkboxes.get(activateOnBatch));
panel.add(this.checkboxes.get(noActionIfCompileErrors));
panel.add(this.checkboxes.get(processAsync));
panel.add(Box.createHorizontalGlue());
panel.setMinimumSize(new Dimension(Short.MAX_VALUE, 0));
return panel;
Expand Down

0 comments on commit 3bb59f0

Please sign in to comment.