blob: c67bc6a4f1b154967aeab3153cdf986f7493147a [file] [log] [blame]
/*
* Copyright 2000-2013 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.codeInsight.actions;
import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.lang.LanguageFormatting;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.util.ProgressWindow;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectCoreUtil;
import com.intellij.openapi.roots.GeneratedSourcesFilter;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.ex.MessagesEx;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiBundle;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.SequentialModalProgressTask;
import com.intellij.util.SequentialTask;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public abstract class AbstractLayoutCodeProcessor {
private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor");
protected final Project myProject;
private final Module myModule;
private PsiDirectory myDirectory;
private PsiFile myFile;
private List<PsiFile> myFiles;
private boolean myIncludeSubdirs;
private final String myProgressText;
private final String myCommandName;
private final Runnable myPostRunnable;
private final boolean myProcessChangedTextOnly;
protected AbstractLayoutCodeProcessor myPreviousCodeProcessor;
private List<FileFilter> myFilters = ContainerUtil.newArrayList();
protected AbstractLayoutCodeProcessor(Project project, String commandName, String progressText, boolean processChangedTextOnly) {
this(project, (Module)null, commandName, progressText, processChangedTextOnly);
}
protected AbstractLayoutCodeProcessor(@NotNull AbstractLayoutCodeProcessor previous,
@NotNull String commandName,
@NotNull String progressText)
{
myProject = previous.myProject;
myModule = previous.myModule;
myDirectory = previous.myDirectory;
myFile = previous.myFile;
myFiles = previous.myFiles;
myIncludeSubdirs = previous.myIncludeSubdirs;
myProcessChangedTextOnly = previous.myProcessChangedTextOnly;
myPostRunnable = null;
myProgressText = progressText;
myCommandName = commandName;
myPreviousCodeProcessor = previous;
myFilters = previous.myFilters;
}
protected AbstractLayoutCodeProcessor(Project project,
@Nullable Module module,
String commandName,
String progressText,
boolean processChangedTextOnly)
{
myProject = project;
myModule = module;
myDirectory = null;
myIncludeSubdirs = true;
myCommandName = commandName;
myProgressText = progressText;
myPostRunnable = null;
myProcessChangedTextOnly = processChangedTextOnly;
}
protected AbstractLayoutCodeProcessor(Project project,
PsiDirectory directory,
boolean includeSubdirs,
String progressText,
String commandName,
boolean processChangedTextOnly)
{
myProject = project;
myModule = null;
myDirectory = directory;
myIncludeSubdirs = includeSubdirs;
myProgressText = progressText;
myCommandName = commandName;
myPostRunnable = null;
myProcessChangedTextOnly = processChangedTextOnly;
}
protected AbstractLayoutCodeProcessor(Project project,
PsiFile file,
String progressText,
String commandName,
boolean processChangedTextOnly)
{
myProject = project;
myModule = null;
myFile = file;
myProgressText = progressText;
myCommandName = commandName;
myPostRunnable = null;
myProcessChangedTextOnly = processChangedTextOnly;
}
protected AbstractLayoutCodeProcessor(Project project,
PsiFile[] files,
String progressText,
String commandName,
@Nullable Runnable postRunnable,
boolean processChangedTextOnly)
{
myProject = project;
myModule = null;
myFiles = filterFilesTo(files, new ArrayList<PsiFile>());
myProgressText = progressText;
myCommandName = commandName;
myPostRunnable = postRunnable;
myProcessChangedTextOnly = processChangedTextOnly;
}
private static List<PsiFile> filterFilesTo(PsiFile[] files, List<PsiFile> list) {
for (PsiFile file : files) {
if (canBeFormatted(file)) {
list.add(file);
}
}
return list;
}
@Nullable
private FutureTask<Boolean> getPreviousProcessorTask(@NotNull PsiFile file, boolean processChangedTextOnly) {
return myPreviousCodeProcessor != null ? myPreviousCodeProcessor.preprocessFile(file, processChangedTextOnly)
: null;
}
public void addFileFilter(@NotNull FileFilter filter) {
myFilters.add(filter);
}
/**
* Ensures that given file is ready to reformatting and prepares it if necessary.
*
* @param file file to process
* @param processChangedTextOnly flag that defines is only the changed text (in terms of VCS change) should be processed
* @return task that triggers formatting of the given file. Returns value of that task indicates whether formatting
* is finished correctly or not (exception occurred, user cancelled formatting etc)
* @throws IncorrectOperationException if unexpected exception occurred during formatting
*/
@NotNull
protected abstract FutureTask<Boolean> prepareTask(@NotNull PsiFile file, boolean processChangedTextOnly) throws IncorrectOperationException;
public FutureTask<Boolean> preprocessFile(@NotNull PsiFile file, boolean processChangedTextOnly) throws IncorrectOperationException {
final FutureTask<Boolean> previousTask = getPreviousProcessorTask(file, processChangedTextOnly);
final FutureTask<Boolean> currentTask = prepareTask(file, processChangedTextOnly);
return new FutureTask<Boolean>(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
if (previousTask != null) {
previousTask.run();
if (!previousTask.get() || previousTask.isCancelled()) return false;
}
currentTask.run();
return currentTask.get() && !currentTask.isCancelled();
}
});
}
public void run() {
if (myFile != null) {
runProcessFile(myFile);
return;
}
FileTreeIterator iterator;
if (myFiles != null) {
iterator = new FileTreeIterator(myFiles);
}
else {
iterator = myProcessChangedTextOnly ? buildChangedFilesIterator()
: buildFileTreeIterator();
}
runProcessFiles(iterator);
}
private FileTreeIterator buildFileTreeIterator() {
if (myDirectory != null) {
return new FileTreeIterator(myDirectory);
}
else if (myFiles != null) {
return new FileTreeIterator(myFiles);
}
else if (myModule != null) {
return new FileTreeIterator(myModule);
}
else if (myProject != null) {
return new FileTreeIterator(myProject);
}
return new FileTreeIterator(Collections.<PsiFile>emptyList());
}
@NotNull
private FileTreeIterator buildChangedFilesIterator() {
List<PsiFile> files = getChangedFilesFromContext();
return new FileTreeIterator(files);
}
@NotNull
private List<PsiFile> getChangedFilesFromContext() {
List<PsiDirectory> dirs = getAllSearchableDirsFromContext();
return FormatChangedTextUtil.getChangedFilesFromDirs(myProject, dirs);
}
private List<PsiDirectory> getAllSearchableDirsFromContext() {
List<PsiDirectory> dirs = ContainerUtil.newArrayList();
if (myDirectory != null) {
dirs.add(myDirectory);
}
else if (myModule != null) {
List<PsiDirectory> allModuleDirs = FileTreeIterator.collectModuleDirectories(myModule);
dirs.addAll(allModuleDirs);
}
else if (myProject != null) {
List<PsiDirectory> allProjectDirs = FileTreeIterator.collectProjectDirectories(myProject);
dirs.addAll(allProjectDirs);
}
return dirs;
}
private void runProcessFile(@NotNull final PsiFile file) {
Document document = PsiDocumentManager.getInstance(myProject).getDocument(file);
if (document == null) {
return;
}
if (!FileDocumentManager.getInstance().requestWriting(document, myProject)) {
Messages.showMessageDialog(myProject, PsiBundle.message("cannot.modify.a.read.only.file", file.getName()),
CodeInsightBundle.message("error.dialog.readonly.file.title"),
Messages.getErrorIcon()
);
return;
}
final Runnable[] resultRunnable = new Runnable[1];
Runnable readAction = new Runnable() {
@Override
public void run() {
if (!checkFileWritable(file)) return;
try{
resultRunnable[0] = preprocessFile(file, myProcessChangedTextOnly);
}
catch(IncorrectOperationException e){
LOG.error(e);
}
}
};
Runnable writeAction = new Runnable() {
@Override
public void run() {
if (resultRunnable[0] != null) {
resultRunnable[0].run();
}
}
};
runLayoutCodeProcess(readAction, writeAction, false );
}
private boolean checkFileWritable(final PsiFile file){
if (!file.isWritable()){
MessagesEx.fileIsReadOnly(myProject, file.getVirtualFile())
.setTitle(CodeInsightBundle.message("error.dialog.readonly.file.title"))
.showLater();
return false;
}
else{
return true;
}
}
@Nullable
private Runnable createIterativeFileProcessor(@NotNull final FileTreeIterator fileIterator) {
return new Runnable() {
@Override
public void run() {
SequentialModalProgressTask progressTask = new SequentialModalProgressTask(myProject, myCommandName);
progressTask.setMinIterationTime(100);
ReformatFilesTask reformatFilesTask = new ReformatFilesTask(fileIterator);
reformatFilesTask.setCompositeTask(progressTask);
progressTask.setTask(reformatFilesTask);
ProgressManager.getInstance().run(progressTask);
}
};
}
private void runProcessFiles(final @NotNull FileTreeIterator fileIterator) {
final Runnable[] resultRunnable = new Runnable[1];
Runnable readAction = new Runnable() {
@Override
public void run() {
resultRunnable[0] = createIterativeFileProcessor(fileIterator);
}
};
Runnable writeAction = new Runnable() {
@Override
public void run() {
if (resultRunnable[0] != null) {
resultRunnable[0].run();
}
}
};
runLayoutCodeProcess(readAction, writeAction, true);
}
private static boolean canBeFormatted(PsiFile file) {
if (LanguageFormatting.INSTANCE.forContext(file) == null) {
return false;
}
VirtualFile virtualFile = file.getVirtualFile();
if (virtualFile == null) return true;
if (ProjectCoreUtil.isProjectOrWorkspaceFile(virtualFile)) return false;
for (GeneratedSourcesFilter filter : GeneratedSourcesFilter.EP_NAME.getExtensions()) {
if (filter.isGeneratedSource(virtualFile, file.getProject())) {
return false;
}
}
return true;
}
private void runLayoutCodeProcess(final Runnable readAction, final Runnable writeAction, final boolean globalAction) {
final ProgressWindow progressWindow = new ProgressWindow(true, myProject);
progressWindow.setTitle(myCommandName);
progressWindow.setText(myProgressText);
final ModalityState modalityState = ModalityState.current();
final Runnable process = new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().runReadAction(readAction);
}
};
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
ProgressManager.getInstance().runProcess(process, progressWindow);
}
catch(ProcessCanceledException e) {
return;
}
catch(IndexNotReadyException e) {
return;
}
final Runnable writeRunnable = new Runnable() {
@Override
public void run() {
CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
@Override
public void run() {
if (globalAction) CommandProcessor.getInstance().markCurrentCommandAsGlobal(myProject);
try {
ApplicationManager.getApplication().runWriteAction(writeAction);
if (myPostRunnable != null) {
ApplicationManager.getApplication().invokeLater(myPostRunnable);
}
}
catch (IndexNotReadyException ignored) {
}
}
}, myCommandName, null);
}
};
if (ApplicationManager.getApplication().isUnitTestMode()) {
writeRunnable.run();
}
else {
ApplicationManager.getApplication().invokeLater(writeRunnable, modalityState, myProject.getDisposed());
}
}
};
if (ApplicationManager.getApplication().isUnitTestMode()) {
runnable.run();
}
else {
ApplicationManager.getApplication().executeOnPooledThread(runnable);
}
}
public void runWithoutProgress() throws IncorrectOperationException {
final Runnable runnable = preprocessFile(myFile, myProcessChangedTextOnly);
runnable.run();
}
private class ReformatFilesTask implements SequentialTask {
private SequentialModalProgressTask myCompositeTask;
private final FileTreeIterator myFileTreeIterator;
private final FileTreeIterator myCountingIterator;
private int myTotalFiles = 0;
private int myFilesProcessed = 0;
private boolean myStopFormatting;
private boolean myFilesCountingFinished;
ReformatFilesTask(@NotNull FileTreeIterator fileIterator) {
myFileTreeIterator = fileIterator;
myCountingIterator = new FileTreeIterator(fileIterator);
}
@Override
public void prepare() {
}
@Override
public boolean isDone() {
return myStopFormatting || !myFileTreeIterator.hasNext();
}
private void countingIteration() {
if (myCountingIterator.hasNext()) {
myCountingIterator.next();
myTotalFiles++;
}
else {
myFilesCountingFinished = true;
}
}
@Override
public boolean iteration() {
if (myStopFormatting) {
return true;
}
if (!myFilesCountingFinished) {
countingIteration();
return true;
}
updateIndicator(myFilesProcessed);
if (myFileTreeIterator.hasNext()) {
PsiFile file = myFileTreeIterator.next();
myFilesProcessed++;
if (file.isWritable() && canBeFormatted(file) && acceptedByFilters(file)) {
performFileProcessing(file);
}
}
return true;
}
private void performFileProcessing(@NotNull PsiFile file) {
FutureTask<Boolean> task = preprocessFile(file, myProcessChangedTextOnly);
task.run();
try {
if (!task.get() || task.isCancelled()) {
myStopFormatting = true;
}
}
catch (InterruptedException e) {
LOG.error("Got unexpected exception during formatting", e);
}
catch (ExecutionException e) {
LOG.error("Got unexpected exception during formatting", e);
}
}
private void updateIndicator(int filesProcessed) {
if (myCompositeTask != null) {
ProgressIndicator indicator = myCompositeTask.getIndicator();
if (indicator != null)
indicator.setFraction((double)filesProcessed / myTotalFiles);
}
}
@Override
public void stop() {
myStopFormatting = true;
}
public void setCompositeTask(@Nullable SequentialModalProgressTask compositeTask) {
myCompositeTask = compositeTask;
}
}
private boolean acceptedByFilters(@NotNull PsiFile file) {
VirtualFile vFile = file.getVirtualFile();
if (vFile == null) {
return false;
}
for (FileFilter filter : myFilters) {
if (!filter.accept(file.getVirtualFile())) {
return false;
}
}
return true;
}
}