Do not log CancellationException
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / actions / AbstractLayoutCodeProcessor.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package com.intellij.codeInsight.actions;
18
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;
58
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;
66
67 public abstract class AbstractLayoutCodeProcessor {
68   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor");
69
70   protected final Project myProject;
71   private final Module myModule;
72
73   private PsiDirectory myDirectory;
74   private PsiFile myFile;
75   private List<PsiFile> myFiles;
76   private boolean myIncludeSubdirs;
77
78   private final String myProgressText;
79   private final String myCommandName;
80   private Runnable myPostRunnable;
81   private boolean myProcessChangedTextOnly;
82
83   protected AbstractLayoutCodeProcessor myPreviousCodeProcessor;
84   private List<FileFilter> myFilters = ContainerUtil.newArrayList();
85
86   private LayoutCodeInfoCollector myInfoCollector;
87
88   protected AbstractLayoutCodeProcessor(Project project, String commandName, String progressText, boolean processChangedTextOnly) {
89     this(project, (Module)null, commandName, progressText, processChangedTextOnly);
90   }
91
92   protected AbstractLayoutCodeProcessor(@NotNull AbstractLayoutCodeProcessor previous,
93                                         @NotNull String commandName,
94                                         @NotNull String progressText)
95   {
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;
103
104     myPostRunnable = null;
105     myProgressText = progressText;
106     myCommandName = commandName;
107     myPreviousCodeProcessor = previous;
108     myFilters = previous.myFilters;
109     myInfoCollector = previous.myInfoCollector;
110   }
111
112   protected AbstractLayoutCodeProcessor(Project project,
113                                         @Nullable Module module,
114                                         String commandName,
115                                         String progressText,
116                                         boolean processChangedTextOnly)
117   {
118     myProject = project;
119     myModule = module;
120     myDirectory = null;
121     myIncludeSubdirs = true;
122     myCommandName = commandName;
123     myProgressText = progressText;
124     myPostRunnable = null;
125     myProcessChangedTextOnly = processChangedTextOnly;
126   }
127
128   protected AbstractLayoutCodeProcessor(Project project,
129                                         PsiDirectory directory,
130                                         boolean includeSubdirs,
131                                         String progressText,
132                                         String commandName,
133                                         boolean processChangedTextOnly)
134   {
135     myProject = project;
136     myModule = null;
137     myDirectory = directory;
138     myIncludeSubdirs = includeSubdirs;
139     myProgressText = progressText;
140     myCommandName = commandName;
141     myPostRunnable = null;
142     myProcessChangedTextOnly = processChangedTextOnly;
143   }
144
145   protected AbstractLayoutCodeProcessor(Project project,
146                                         PsiFile file,
147                                         String progressText,
148                                         String commandName,
149                                         boolean processChangedTextOnly)
150   {
151     myProject = project;
152     myModule = null;
153     myFile = file;
154     myProgressText = progressText;
155     myCommandName = commandName;
156     myPostRunnable = null;
157     myProcessChangedTextOnly = processChangedTextOnly;
158   }
159
160   protected AbstractLayoutCodeProcessor(Project project,
161                                         PsiFile[] files,
162                                         String progressText,
163                                         String commandName,
164                                         @Nullable Runnable postRunnable,
165                                         boolean processChangedTextOnly)
166   {
167     myProject = project;
168     myModule = null;
169     myFiles = filterFilesTo(files, new ArrayList<PsiFile>());
170     myProgressText = progressText;
171     myCommandName = commandName;
172     myPostRunnable = postRunnable;
173     myProcessChangedTextOnly = processChangedTextOnly;
174   }
175
176   private static List<PsiFile> filterFilesTo(PsiFile[] files, List<PsiFile> list) {
177     for (PsiFile file : files) {
178       if (canBeFormatted(file)) {
179         list.add(file);
180       }
181     }
182     return list;
183   }
184
185   public void setPostRunnable(Runnable postRunnable) {
186     myPostRunnable = postRunnable;
187   }
188
189   @Nullable
190   private FutureTask<Boolean> getPreviousProcessorTask(@NotNull PsiFile file, boolean processChangedTextOnly) {
191     return myPreviousCodeProcessor != null ? myPreviousCodeProcessor.preprocessFile(file, processChangedTextOnly)
192                                            : null;
193   }
194
195   public void setCollectInfo(boolean isCollectInfo) {
196     myInfoCollector = isCollectInfo ? new LayoutCodeInfoCollector() : null;
197
198     AbstractLayoutCodeProcessor current = this;
199     while (current.myPreviousCodeProcessor != null) {
200       current = current.myPreviousCodeProcessor;
201       current.myInfoCollector = myInfoCollector;
202     }
203   }
204
205   public void addFileFilter(@NotNull FileFilter filter) {
206     myFilters.add(filter);
207   }
208
209   protected void setProcessChangedTextOnly(boolean value) {
210     myProcessChangedTextOnly = value;
211   }
212   /**
213    * Ensures that given file is ready to reformatting and prepares it if necessary.
214    *
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
220    */
221   @NotNull
222   protected abstract FutureTask<Boolean> prepareTask(@NotNull PsiFile file, boolean processChangedTextOnly) throws IncorrectOperationException;
223
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);
227
228     return new FutureTask<Boolean>(new Callable<Boolean>() {
229       @Override
230       public Boolean call() throws Exception {
231         if (previousTask != null) {
232           previousTask.run();
233           if (!previousTask.get() || previousTask.isCancelled()) return false;
234         }
235
236         ApplicationManager.getApplication().runWriteAction(new Runnable() {
237           @Override
238           public void run() {
239             currentTask.run();
240           }
241         });
242
243         return currentTask.get() && !currentTask.isCancelled();
244       }
245     });
246   }
247
248   public void run() {
249     if (myFile != null) {
250       runProcessFile(myFile);
251       return;
252     }
253
254     FileTreeIterator iterator;
255     if (myFiles != null) {
256       iterator = new FileTreeIterator(myFiles);
257     }
258     else {
259       iterator = myProcessChangedTextOnly ? buildChangedFilesIterator()
260                                           : buildFileTreeIterator();
261     }
262     runProcessFiles(iterator);
263   }
264
265   private FileTreeIterator buildFileTreeIterator() {
266     if (myDirectory != null) {
267       return new FileTreeIterator(myDirectory);
268     }
269     else if (myFiles != null) {
270       return new FileTreeIterator(myFiles);
271     }
272     else if (myModule != null) {
273       return new FileTreeIterator(myModule);
274     }
275     else if (myProject != null) {
276       return new FileTreeIterator(myProject);
277     }
278
279     return new FileTreeIterator(Collections.<PsiFile>emptyList());
280   }
281
282   @NotNull
283   private FileTreeIterator buildChangedFilesIterator() {
284     List<PsiFile> files = getChangedFilesFromContext();
285     return new FileTreeIterator(files);
286   }
287
288   @NotNull
289   private List<PsiFile> getChangedFilesFromContext() {
290     List<PsiDirectory> dirs = getAllSearchableDirsFromContext();
291     return FormatChangedTextUtil.getChangedFilesFromDirs(myProject, dirs);
292   }
293
294   private List<PsiDirectory> getAllSearchableDirsFromContext() {
295     List<PsiDirectory> dirs = ContainerUtil.newArrayList();
296     if (myDirectory != null) {
297       dirs.add(myDirectory);
298     }
299     else if (myModule != null) {
300       List<PsiDirectory> allModuleDirs = FileTreeIterator.collectModuleDirectories(myModule);
301       dirs.addAll(allModuleDirs);
302     }
303     else if (myProject != null) {
304       List<PsiDirectory> allProjectDirs = FileTreeIterator.collectProjectDirectories(myProject);
305       dirs.addAll(allProjectDirs);
306     }
307     return dirs;
308   }
309
310
311   private void runProcessFile(@NotNull final PsiFile file) {
312     Document document = PsiDocumentManager.getInstance(myProject).getDocument(file);
313
314     if (document == null) {
315       return;
316     }
317
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()
322       );
323       return;
324     }
325
326     final Ref<FutureTask<Boolean>> writeActionRunnable = new Ref<FutureTask<Boolean>>();
327     Runnable readAction = new Runnable() {
328       @Override
329       public void run() {
330         if (!checkFileWritable(file)) return;
331         try{
332           FutureTask<Boolean> writeTask = preprocessFile(file, myProcessChangedTextOnly);
333           writeActionRunnable.set(writeTask);
334         }
335         catch(IncorrectOperationException e){
336           LOG.error(e);
337         }
338       }
339     };
340     Runnable writeAction = new Runnable() {
341       @Override
342       public void run() {
343         if (writeActionRunnable.isNull()) return;
344         FutureTask<Boolean> task = writeActionRunnable.get();
345         task.run();
346         try {
347           task.get();
348         }
349         catch (CancellationException ignored) {
350         }
351         catch (Exception e) {
352           LOG.error(e);
353         }
354       }
355     };
356     runLayoutCodeProcess(readAction, writeAction, false );
357   }
358
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"))
363           .showLater();
364       return false;
365     }
366     else{
367       return true;
368     }
369   }
370
371   @Nullable
372   private Runnable createIterativeFileProcessor(@NotNull final FileTreeIterator fileIterator) {
373     return new Runnable() {
374       @Override
375       public void run() {
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);
382       }
383     };
384   }
385
386   private void runProcessFiles(@NotNull final FileTreeIterator fileIterator) {
387     final Runnable[] resultRunnable = new Runnable[1];
388
389     Runnable readAction = new Runnable() {
390       @Override
391       public void run() {
392         resultRunnable[0] = createIterativeFileProcessor(fileIterator);
393       }
394     };
395
396     Runnable writeAction = new Runnable() {
397       @Override
398       public void run() {
399         if (resultRunnable[0] != null) {
400           resultRunnable[0].run();
401         }
402       }
403     };
404
405     runLayoutCodeProcess(readAction, writeAction, true);
406   }
407
408   private static boolean canBeFormatted(PsiFile file) {
409     if (LanguageFormatting.INSTANCE.forContext(file) == null) {
410       return false;
411     }
412     VirtualFile virtualFile = file.getVirtualFile();
413     if (virtualFile == null) return true;
414
415     if (ProjectCoreUtil.isProjectOrWorkspaceFile(virtualFile)) return false;
416
417     for (GeneratedSourcesFilter filter : GeneratedSourcesFilter.EP_NAME.getExtensions()) {
418       if (filter.isGeneratedSource(virtualFile, file.getProject())) {
419         return false;
420       }
421     }
422     return true;
423   }
424
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);
429
430     final ModalityState modalityState = ModalityState.current();
431
432     final Runnable process = new Runnable() {
433       @Override
434       public void run() {
435         ApplicationManager.getApplication().runReadAction(readAction);
436       }
437     };
438
439     Runnable runnable = new Runnable() {
440       @Override
441       public void run() {
442         try {
443           ProgressManager.getInstance().runProcess(process, progressWindow);
444         }
445         catch(ProcessCanceledException e) {
446           return;
447         }
448         catch(IndexNotReadyException e) {
449           return;
450         }
451
452         final Runnable writeRunnable = new Runnable() {
453           @Override
454           public void run() {
455             CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
456               @Override
457               public void run() {
458                 if (globalAction) CommandProcessor.getInstance().markCurrentCommandAsGlobal(myProject);
459                 try {
460                   writeAction.run();
461
462                   if (myPostRunnable != null) {
463                     ApplicationManager.getApplication().invokeLater(myPostRunnable);
464                   }
465                 }
466                 catch (IndexNotReadyException ignored) {
467                 }
468               }
469             }, myCommandName, null);
470           }
471         };
472
473         if (ApplicationManager.getApplication().isUnitTestMode()) {
474           writeRunnable.run();
475         }
476         else {
477           ApplicationManager.getApplication().invokeLater(writeRunnable, modalityState, myProject.getDisposed());
478         }
479       }
480     };
481
482     if (ApplicationManager.getApplication().isUnitTestMode()) {
483       runnable.run();
484     }
485     else {
486       ApplicationManager.getApplication().executeOnPooledThread(runnable);
487     }
488   }
489
490   public void runWithoutProgress() throws IncorrectOperationException {
491     final Runnable runnable = preprocessFile(myFile, myProcessChangedTextOnly);
492     runnable.run();
493   }
494
495   private class ReformatFilesTask implements SequentialTask {
496     private SequentialModalProgressTask myCompositeTask;
497
498     private final FileTreeIterator myFileTreeIterator;
499     private final FileTreeIterator myCountingIterator;
500
501     private int myTotalFiles = 0;
502     private int myFilesProcessed = 0;
503     private boolean myStopFormatting;
504     private boolean myFilesCountingFinished;
505
506     ReformatFilesTask(@NotNull FileTreeIterator fileIterator) {
507       myFileTreeIterator = fileIterator;
508       myCountingIterator = new FileTreeIterator(fileIterator);
509     }
510
511     @Override
512     public void prepare() {
513     }
514
515     @Override
516     public boolean isDone() {
517       return myStopFormatting || !myFileTreeIterator.hasNext();
518     }
519
520     private void countingIteration() {
521       if (myCountingIterator.hasNext()) {
522         myCountingIterator.next();
523         myTotalFiles++;
524       }
525       else {
526         myFilesCountingFinished = true;
527       }
528     }
529
530     @Override
531     public boolean iteration() {
532       if (myStopFormatting) {
533         return true;
534       }
535
536       if (!myFilesCountingFinished) {
537         updateIndicatorText(ApplicationBundle.message("bulk.reformat.prepare.progress.text"), "");
538         countingIteration();
539         return true;
540       }
541
542       updateIndicatorFraction(myFilesProcessed);
543
544       if (myFileTreeIterator.hasNext()) {
545         final PsiFile file = myFileTreeIterator.next();
546         myFilesProcessed++;
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() {
550             @Override
551             public void run() {
552               performFileProcessing(file);
553             }
554           });
555         }
556       }
557
558       return true;
559     }
560
561     private void performFileProcessing(@NotNull PsiFile file) {
562       FutureTask<Boolean> task = preprocessFile(file, myProcessChangedTextOnly);
563       task.run();
564       try {
565         if (!task.get() || task.isCancelled()) {
566           myStopFormatting = true;
567         }
568       }
569       catch (InterruptedException e) {
570         LOG.error("Got unexpected exception during formatting", e);
571       }
572       catch (ExecutionException e) {
573         LOG.error("Got unexpected exception during formatting", e);
574       }
575     }
576
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);
582       }
583     }
584
585     private String getPresentablePath(@NotNull PsiFile file) {
586       VirtualFile vFile = file.getVirtualFile();
587       return vFile != null ? ProjectUtil.calcRelativeToProjectPath(vFile, myProject) : file.getName();
588     }
589
590     private void updateIndicatorFraction(int processed) {
591       ProgressIndicator indicator = myCompositeTask.getIndicator();
592       if (indicator != null) {
593         indicator.setFraction((double)processed / myTotalFiles);
594       }
595     }
596
597     @Override
598     public void stop() {
599       myStopFormatting = true;
600     }
601
602     public void setCompositeTask(@Nullable SequentialModalProgressTask compositeTask) {
603       myCompositeTask = compositeTask;
604     }
605   }
606
607   private boolean acceptedByFilters(@NotNull PsiFile file) {
608     VirtualFile vFile = file.getVirtualFile();
609     if (vFile == null) {
610       return false;
611     }
612
613     for (FileFilter filter : myFilters) {
614       if (!filter.accept(file.getVirtualFile())) {
615         return false;
616       }
617     }
618
619     return true;
620   }
621
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());
626       ranges.add(range);
627     }
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]));
633       }
634     }
635
636     return ranges;
637   }
638
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());
647     }
648   }
649
650   @Nullable
651   public LayoutCodeInfoCollector getInfoCollector() {
652     return myInfoCollector;
653   }
654 }