2 * Copyright 2000-2014 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.intellij.codeInsight.actions;
19 import com.intellij.codeInsight.CodeInsightBundle;
20 import com.intellij.lang.LanguageFormatting;
21 import com.intellij.notification.Notification;
22 import com.intellij.notification.NotificationType;
23 import com.intellij.openapi.application.ApplicationBundle;
24 import com.intellij.openapi.application.ApplicationManager;
25 import com.intellij.openapi.application.ModalityState;
26 import com.intellij.openapi.command.CommandProcessor;
27 import com.intellij.openapi.diagnostic.Logger;
28 import com.intellij.openapi.editor.Document;
29 import com.intellij.openapi.editor.SelectionModel;
30 import com.intellij.openapi.fileEditor.FileDocumentManager;
31 import com.intellij.openapi.module.Module;
32 import com.intellij.openapi.progress.ProcessCanceledException;
33 import com.intellij.openapi.progress.ProgressIndicator;
34 import com.intellij.openapi.progress.ProgressManager;
35 import com.intellij.openapi.progress.util.ProgressWindow;
36 import com.intellij.openapi.project.IndexNotReadyException;
37 import com.intellij.openapi.project.Project;
38 import com.intellij.openapi.project.ProjectCoreUtil;
39 import com.intellij.openapi.project.ProjectUtil;
40 import com.intellij.openapi.roots.GeneratedSourcesFilter;
41 import com.intellij.openapi.ui.Messages;
42 import com.intellij.openapi.ui.ex.MessagesEx;
43 import com.intellij.openapi.util.Ref;
44 import com.intellij.openapi.util.TextRange;
45 import com.intellij.openapi.vfs.VirtualFile;
46 import com.intellij.psi.PsiBundle;
47 import com.intellij.psi.PsiDirectory;
48 import com.intellij.psi.PsiDocumentManager;
49 import com.intellij.psi.PsiFile;
50 import com.intellij.util.IncorrectOperationException;
51 import com.intellij.util.SequentialModalProgressTask;
52 import com.intellij.util.SequentialTask;
53 import com.intellij.util.SmartList;
54 import com.intellij.util.containers.ContainerUtil;
55 import com.intellij.util.diff.FilesTooBigForDiffException;
56 import org.jetbrains.annotations.NotNull;
57 import org.jetbrains.annotations.Nullable;
59 import java.util.ArrayList;
60 import java.util.Collections;
61 import java.util.List;
62 import java.util.concurrent.Callable;
63 import java.util.concurrent.CancellationException;
64 import java.util.concurrent.ExecutionException;
65 import java.util.concurrent.FutureTask;
67 public abstract class AbstractLayoutCodeProcessor {
68 private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor");
70 protected final Project myProject;
71 private final Module myModule;
73 private PsiDirectory myDirectory;
74 private PsiFile myFile;
75 private List<PsiFile> myFiles;
76 private boolean myIncludeSubdirs;
78 private final String myProgressText;
79 private final String myCommandName;
80 private Runnable myPostRunnable;
81 private boolean myProcessChangedTextOnly;
83 protected AbstractLayoutCodeProcessor myPreviousCodeProcessor;
84 private List<FileFilter> myFilters = ContainerUtil.newArrayList();
86 private LayoutCodeInfoCollector myInfoCollector;
88 protected AbstractLayoutCodeProcessor(Project project, String commandName, String progressText, boolean processChangedTextOnly) {
89 this(project, (Module)null, commandName, progressText, processChangedTextOnly);
92 protected AbstractLayoutCodeProcessor(@NotNull AbstractLayoutCodeProcessor previous,
93 @NotNull String commandName,
94 @NotNull String progressText)
96 myProject = previous.myProject;
97 myModule = previous.myModule;
98 myDirectory = previous.myDirectory;
99 myFile = previous.myFile;
100 myFiles = previous.myFiles;
101 myIncludeSubdirs = previous.myIncludeSubdirs;
102 myProcessChangedTextOnly = previous.myProcessChangedTextOnly;
104 myPostRunnable = null;
105 myProgressText = progressText;
106 myCommandName = commandName;
107 myPreviousCodeProcessor = previous;
108 myFilters = previous.myFilters;
109 myInfoCollector = previous.myInfoCollector;
112 protected AbstractLayoutCodeProcessor(Project project,
113 @Nullable Module module,
116 boolean processChangedTextOnly)
121 myIncludeSubdirs = true;
122 myCommandName = commandName;
123 myProgressText = progressText;
124 myPostRunnable = null;
125 myProcessChangedTextOnly = processChangedTextOnly;
128 protected AbstractLayoutCodeProcessor(Project project,
129 PsiDirectory directory,
130 boolean includeSubdirs,
133 boolean processChangedTextOnly)
137 myDirectory = directory;
138 myIncludeSubdirs = includeSubdirs;
139 myProgressText = progressText;
140 myCommandName = commandName;
141 myPostRunnable = null;
142 myProcessChangedTextOnly = processChangedTextOnly;
145 protected AbstractLayoutCodeProcessor(Project project,
149 boolean processChangedTextOnly)
154 myProgressText = progressText;
155 myCommandName = commandName;
156 myPostRunnable = null;
157 myProcessChangedTextOnly = processChangedTextOnly;
160 protected AbstractLayoutCodeProcessor(Project project,
164 @Nullable Runnable postRunnable,
165 boolean processChangedTextOnly)
169 myFiles = filterFilesTo(files, new ArrayList<PsiFile>());
170 myProgressText = progressText;
171 myCommandName = commandName;
172 myPostRunnable = postRunnable;
173 myProcessChangedTextOnly = processChangedTextOnly;
176 private static List<PsiFile> filterFilesTo(PsiFile[] files, List<PsiFile> list) {
177 for (PsiFile file : files) {
178 if (canBeFormatted(file)) {
185 public void setPostRunnable(Runnable postRunnable) {
186 myPostRunnable = postRunnable;
190 private FutureTask<Boolean> getPreviousProcessorTask(@NotNull PsiFile file, boolean processChangedTextOnly) {
191 return myPreviousCodeProcessor != null ? myPreviousCodeProcessor.preprocessFile(file, processChangedTextOnly)
195 public void setCollectInfo(boolean isCollectInfo) {
196 myInfoCollector = isCollectInfo ? new LayoutCodeInfoCollector() : null;
198 AbstractLayoutCodeProcessor current = this;
199 while (current.myPreviousCodeProcessor != null) {
200 current = current.myPreviousCodeProcessor;
201 current.myInfoCollector = myInfoCollector;
205 public void addFileFilter(@NotNull FileFilter filter) {
206 myFilters.add(filter);
209 protected void setProcessChangedTextOnly(boolean value) {
210 myProcessChangedTextOnly = value;
213 * Ensures that given file is ready to reformatting and prepares it if necessary.
215 * @param file file to process
216 * @param processChangedTextOnly flag that defines is only the changed text (in terms of VCS change) should be processed
217 * @return task that triggers formatting of the given file. Returns value of that task indicates whether formatting
218 * is finished correctly or not (exception occurred, user cancelled formatting etc)
219 * @throws IncorrectOperationException if unexpected exception occurred during formatting
222 protected abstract FutureTask<Boolean> prepareTask(@NotNull PsiFile file, boolean processChangedTextOnly) throws IncorrectOperationException;
224 public FutureTask<Boolean> preprocessFile(@NotNull PsiFile file, boolean processChangedTextOnly) throws IncorrectOperationException {
225 final FutureTask<Boolean> previousTask = getPreviousProcessorTask(file, processChangedTextOnly);
226 final FutureTask<Boolean> currentTask = prepareTask(file, processChangedTextOnly);
228 return new FutureTask<Boolean>(new Callable<Boolean>() {
230 public Boolean call() throws Exception {
231 if (previousTask != null) {
233 if (!previousTask.get() || previousTask.isCancelled()) return false;
236 ApplicationManager.getApplication().runWriteAction(new Runnable() {
243 return currentTask.get() && !currentTask.isCancelled();
249 if (myFile != null) {
250 runProcessFile(myFile);
254 FileTreeIterator iterator;
255 if (myFiles != null) {
256 iterator = new FileTreeIterator(myFiles);
259 iterator = myProcessChangedTextOnly ? buildChangedFilesIterator()
260 : buildFileTreeIterator();
262 runProcessFiles(iterator);
265 private FileTreeIterator buildFileTreeIterator() {
266 if (myDirectory != null) {
267 return new FileTreeIterator(myDirectory);
269 else if (myFiles != null) {
270 return new FileTreeIterator(myFiles);
272 else if (myModule != null) {
273 return new FileTreeIterator(myModule);
275 else if (myProject != null) {
276 return new FileTreeIterator(myProject);
279 return new FileTreeIterator(Collections.<PsiFile>emptyList());
283 private FileTreeIterator buildChangedFilesIterator() {
284 List<PsiFile> files = getChangedFilesFromContext();
285 return new FileTreeIterator(files);
289 private List<PsiFile> getChangedFilesFromContext() {
290 List<PsiDirectory> dirs = getAllSearchableDirsFromContext();
291 return FormatChangedTextUtil.getChangedFilesFromDirs(myProject, dirs);
294 private List<PsiDirectory> getAllSearchableDirsFromContext() {
295 List<PsiDirectory> dirs = ContainerUtil.newArrayList();
296 if (myDirectory != null) {
297 dirs.add(myDirectory);
299 else if (myModule != null) {
300 List<PsiDirectory> allModuleDirs = FileTreeIterator.collectModuleDirectories(myModule);
301 dirs.addAll(allModuleDirs);
303 else if (myProject != null) {
304 List<PsiDirectory> allProjectDirs = FileTreeIterator.collectProjectDirectories(myProject);
305 dirs.addAll(allProjectDirs);
311 private void runProcessFile(@NotNull final PsiFile file) {
312 Document document = PsiDocumentManager.getInstance(myProject).getDocument(file);
314 if (document == null) {
318 if (!FileDocumentManager.getInstance().requestWriting(document, myProject)) {
319 Messages.showMessageDialog(myProject, PsiBundle.message("cannot.modify.a.read.only.file", file.getName()),
320 CodeInsightBundle.message("error.dialog.readonly.file.title"),
321 Messages.getErrorIcon()
326 final Ref<FutureTask<Boolean>> writeActionRunnable = new Ref<FutureTask<Boolean>>();
327 Runnable readAction = new Runnable() {
330 if (!checkFileWritable(file)) return;
332 FutureTask<Boolean> writeTask = preprocessFile(file, myProcessChangedTextOnly);
333 writeActionRunnable.set(writeTask);
335 catch(IncorrectOperationException e){
340 Runnable writeAction = new Runnable() {
343 if (writeActionRunnable.isNull()) return;
344 FutureTask<Boolean> task = writeActionRunnable.get();
349 catch (CancellationException ignored) {
351 catch (Exception e) {
356 runLayoutCodeProcess(readAction, writeAction, false );
359 private boolean checkFileWritable(final PsiFile file){
360 if (!file.isWritable()){
361 MessagesEx.fileIsReadOnly(myProject, file.getVirtualFile())
362 .setTitle(CodeInsightBundle.message("error.dialog.readonly.file.title"))
372 private Runnable createIterativeFileProcessor(@NotNull final FileTreeIterator fileIterator) {
373 return new Runnable() {
376 SequentialModalProgressTask progressTask = new SequentialModalProgressTask(myProject, myCommandName);
377 progressTask.setMinIterationTime(100);
378 ReformatFilesTask reformatFilesTask = new ReformatFilesTask(fileIterator);
379 reformatFilesTask.setCompositeTask(progressTask);
380 progressTask.setTask(reformatFilesTask);
381 ProgressManager.getInstance().run(progressTask);
386 private void runProcessFiles(@NotNull final FileTreeIterator fileIterator) {
387 final Runnable[] resultRunnable = new Runnable[1];
389 Runnable readAction = new Runnable() {
392 resultRunnable[0] = createIterativeFileProcessor(fileIterator);
396 Runnable writeAction = new Runnable() {
399 if (resultRunnable[0] != null) {
400 resultRunnable[0].run();
405 runLayoutCodeProcess(readAction, writeAction, true);
408 private static boolean canBeFormatted(PsiFile file) {
409 if (LanguageFormatting.INSTANCE.forContext(file) == null) {
412 VirtualFile virtualFile = file.getVirtualFile();
413 if (virtualFile == null) return true;
415 if (ProjectCoreUtil.isProjectOrWorkspaceFile(virtualFile)) return false;
417 for (GeneratedSourcesFilter filter : GeneratedSourcesFilter.EP_NAME.getExtensions()) {
418 if (filter.isGeneratedSource(virtualFile, file.getProject())) {
425 private void runLayoutCodeProcess(final Runnable readAction, final Runnable writeAction, final boolean globalAction) {
426 final ProgressWindow progressWindow = new ProgressWindow(true, myProject);
427 progressWindow.setTitle(myCommandName);
428 progressWindow.setText(myProgressText);
430 final ModalityState modalityState = ModalityState.current();
432 final Runnable process = new Runnable() {
435 ApplicationManager.getApplication().runReadAction(readAction);
439 Runnable runnable = new Runnable() {
443 ProgressManager.getInstance().runProcess(process, progressWindow);
445 catch(ProcessCanceledException e) {
448 catch(IndexNotReadyException e) {
452 final Runnable writeRunnable = new Runnable() {
455 CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
458 if (globalAction) CommandProcessor.getInstance().markCurrentCommandAsGlobal(myProject);
462 if (myPostRunnable != null) {
463 ApplicationManager.getApplication().invokeLater(myPostRunnable);
466 catch (IndexNotReadyException ignored) {
469 }, myCommandName, null);
473 if (ApplicationManager.getApplication().isUnitTestMode()) {
477 ApplicationManager.getApplication().invokeLater(writeRunnable, modalityState, myProject.getDisposed());
482 if (ApplicationManager.getApplication().isUnitTestMode()) {
486 ApplicationManager.getApplication().executeOnPooledThread(runnable);
490 public void runWithoutProgress() throws IncorrectOperationException {
491 final Runnable runnable = preprocessFile(myFile, myProcessChangedTextOnly);
495 private class ReformatFilesTask implements SequentialTask {
496 private SequentialModalProgressTask myCompositeTask;
498 private final FileTreeIterator myFileTreeIterator;
499 private final FileTreeIterator myCountingIterator;
501 private int myTotalFiles = 0;
502 private int myFilesProcessed = 0;
503 private boolean myStopFormatting;
504 private boolean myFilesCountingFinished;
506 ReformatFilesTask(@NotNull FileTreeIterator fileIterator) {
507 myFileTreeIterator = fileIterator;
508 myCountingIterator = new FileTreeIterator(fileIterator);
512 public void prepare() {
516 public boolean isDone() {
517 return myStopFormatting || !myFileTreeIterator.hasNext();
520 private void countingIteration() {
521 if (myCountingIterator.hasNext()) {
522 myCountingIterator.next();
526 myFilesCountingFinished = true;
531 public boolean iteration() {
532 if (myStopFormatting) {
536 if (!myFilesCountingFinished) {
537 updateIndicatorText(ApplicationBundle.message("bulk.reformat.prepare.progress.text"), "");
542 updateIndicatorFraction(myFilesProcessed);
544 if (myFileTreeIterator.hasNext()) {
545 final PsiFile file = myFileTreeIterator.next();
547 if (file.isWritable() && canBeFormatted(file) && acceptedByFilters(file)) {
548 updateIndicatorText(ApplicationBundle.message("bulk.reformat.process.progress.text"), getPresentablePath(file));
549 ApplicationManager.getApplication().runWriteAction(new Runnable() {
552 performFileProcessing(file);
561 private void performFileProcessing(@NotNull PsiFile file) {
562 FutureTask<Boolean> task = preprocessFile(file, myProcessChangedTextOnly);
565 if (!task.get() || task.isCancelled()) {
566 myStopFormatting = true;
569 catch (InterruptedException e) {
570 LOG.error("Got unexpected exception during formatting", e);
572 catch (ExecutionException e) {
573 LOG.error("Got unexpected exception during formatting", e);
577 private void updateIndicatorText(@NotNull String upperLabel, @NotNull String downLabel) {
578 ProgressIndicator indicator = myCompositeTask.getIndicator();
579 if (indicator != null) {
580 indicator.setText(upperLabel);
581 indicator.setText2(downLabel);
585 private String getPresentablePath(@NotNull PsiFile file) {
586 VirtualFile vFile = file.getVirtualFile();
587 return vFile != null ? ProjectUtil.calcRelativeToProjectPath(vFile, myProject) : file.getName();
590 private void updateIndicatorFraction(int processed) {
591 ProgressIndicator indicator = myCompositeTask.getIndicator();
592 if (indicator != null) {
593 indicator.setFraction((double)processed / myTotalFiles);
599 myStopFormatting = true;
602 public void setCompositeTask(@Nullable SequentialModalProgressTask compositeTask) {
603 myCompositeTask = compositeTask;
607 private boolean acceptedByFilters(@NotNull PsiFile file) {
608 VirtualFile vFile = file.getVirtualFile();
613 for (FileFilter filter : myFilters) {
614 if (!filter.accept(file.getVirtualFile())) {
622 protected static List<TextRange> getSelectedRanges(@NotNull SelectionModel selectionModel) {
623 final List<TextRange> ranges = new SmartList<TextRange>();
624 if (selectionModel.hasSelection()) {
625 TextRange range = TextRange.create(selectionModel.getSelectionStart(), selectionModel.getSelectionEnd());
628 else if (selectionModel.hasBlockSelection()) {
629 int[] starts = selectionModel.getBlockSelectionStarts();
630 int[] ends = selectionModel.getBlockSelectionEnds();
631 for (int i = 0; i < starts.length; i++) {
632 ranges.add(TextRange.create(starts[i], ends[i]));
639 protected void handleFileTooBigException(Logger logger, FilesTooBigForDiffException e, @NotNull PsiFile file) {
640 logger.info("Error while calculating changed ranges for: " + file.getVirtualFile(), e);
641 if (!ApplicationManager.getApplication().isUnitTestMode()) {
642 Notification notification = new Notification(ApplicationBundle.message("reformat.changed.text.file.too.big.notification.groupId"),
643 ApplicationBundle.message("reformat.changed.text.file.too.big.notification.title"),
644 ApplicationBundle.message("reformat.changed.text.file.too.big.notification.text", file.getName()),
645 NotificationType.INFORMATION);
646 notification.notify(file.getProject());
651 public LayoutCodeInfoCollector getInfoCollector() {
652 return myInfoCollector;