jps: log only builders that do something noticeable
[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.openapi.application.ApplicationBundle;
22 import com.intellij.openapi.application.ApplicationManager;
23 import com.intellij.openapi.application.ModalityState;
24 import com.intellij.openapi.command.CommandProcessor;
25 import com.intellij.openapi.diagnostic.Logger;
26 import com.intellij.openapi.editor.Document;
27 import com.intellij.openapi.fileEditor.FileDocumentManager;
28 import com.intellij.openapi.module.Module;
29 import com.intellij.openapi.progress.ProcessCanceledException;
30 import com.intellij.openapi.progress.ProgressIndicator;
31 import com.intellij.openapi.progress.ProgressManager;
32 import com.intellij.openapi.progress.util.ProgressWindow;
33 import com.intellij.openapi.project.IndexNotReadyException;
34 import com.intellij.openapi.project.Project;
35 import com.intellij.openapi.project.ProjectCoreUtil;
36 import com.intellij.openapi.project.ProjectUtil;
37 import com.intellij.openapi.roots.GeneratedSourcesFilter;
38 import com.intellij.openapi.ui.Messages;
39 import com.intellij.openapi.ui.ex.MessagesEx;
40 import com.intellij.openapi.vfs.VirtualFile;
41 import com.intellij.psi.PsiBundle;
42 import com.intellij.psi.PsiDirectory;
43 import com.intellij.psi.PsiDocumentManager;
44 import com.intellij.psi.PsiFile;
45 import com.intellij.util.IncorrectOperationException;
46 import com.intellij.util.SequentialModalProgressTask;
47 import com.intellij.util.SequentialTask;
48 import com.intellij.util.containers.ContainerUtil;
49 import org.jetbrains.annotations.NotNull;
50 import org.jetbrains.annotations.Nullable;
51
52 import java.util.ArrayList;
53 import java.util.Collections;
54 import java.util.List;
55 import java.util.concurrent.Callable;
56 import java.util.concurrent.ExecutionException;
57 import java.util.concurrent.FutureTask;
58
59 public abstract class AbstractLayoutCodeProcessor {
60   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor");
61
62   protected final Project myProject;
63   private final Module myModule;
64
65   private PsiDirectory myDirectory;
66   private PsiFile myFile;
67   private List<PsiFile> myFiles;
68   private boolean myIncludeSubdirs;
69
70   private final String myProgressText;
71   private final String myCommandName;
72   private final Runnable myPostRunnable;
73   private boolean myProcessChangedTextOnly;
74
75   protected AbstractLayoutCodeProcessor myPreviousCodeProcessor;
76   private List<FileFilter> myFilters = ContainerUtil.newArrayList();
77
78   protected AbstractLayoutCodeProcessor(Project project, String commandName, String progressText, boolean processChangedTextOnly) {
79     this(project, (Module)null, commandName, progressText, processChangedTextOnly);
80   }
81
82   protected AbstractLayoutCodeProcessor(@NotNull AbstractLayoutCodeProcessor previous,
83                                         @NotNull String commandName,
84                                         @NotNull String progressText)
85   {
86     myProject = previous.myProject;
87     myModule = previous.myModule;
88     myDirectory = previous.myDirectory;
89     myFile = previous.myFile;
90     myFiles = previous.myFiles;
91     myIncludeSubdirs = previous.myIncludeSubdirs;
92     myProcessChangedTextOnly = previous.myProcessChangedTextOnly;
93
94     myPostRunnable = null;
95     myProgressText = progressText;
96     myCommandName = commandName;
97     myPreviousCodeProcessor = previous;
98     myFilters = previous.myFilters;
99   }
100
101   protected AbstractLayoutCodeProcessor(Project project,
102                                         @Nullable Module module,
103                                         String commandName,
104                                         String progressText,
105                                         boolean processChangedTextOnly)
106   {
107     myProject = project;
108     myModule = module;
109     myDirectory = null;
110     myIncludeSubdirs = true;
111     myCommandName = commandName;
112     myProgressText = progressText;
113     myPostRunnable = null;
114     myProcessChangedTextOnly = processChangedTextOnly;
115   }
116
117   protected AbstractLayoutCodeProcessor(Project project,
118                                         PsiDirectory directory,
119                                         boolean includeSubdirs,
120                                         String progressText,
121                                         String commandName,
122                                         boolean processChangedTextOnly)
123   {
124     myProject = project;
125     myModule = null;
126     myDirectory = directory;
127     myIncludeSubdirs = includeSubdirs;
128     myProgressText = progressText;
129     myCommandName = commandName;
130     myPostRunnable = null;
131     myProcessChangedTextOnly = processChangedTextOnly;
132   }
133
134   protected AbstractLayoutCodeProcessor(Project project,
135                                         PsiFile file,
136                                         String progressText,
137                                         String commandName,
138                                         boolean processChangedTextOnly)
139   {
140     myProject = project;
141     myModule = null;
142     myFile = file;
143     myProgressText = progressText;
144     myCommandName = commandName;
145     myPostRunnable = null;
146     myProcessChangedTextOnly = processChangedTextOnly;
147   }
148
149   protected AbstractLayoutCodeProcessor(Project project,
150                                         PsiFile[] files,
151                                         String progressText,
152                                         String commandName,
153                                         @Nullable Runnable postRunnable,
154                                         boolean processChangedTextOnly)
155   {
156     myProject = project;
157     myModule = null;
158     myFiles = filterFilesTo(files, new ArrayList<PsiFile>());
159     myProgressText = progressText;
160     myCommandName = commandName;
161     myPostRunnable = postRunnable;
162     myProcessChangedTextOnly = processChangedTextOnly;
163   }
164
165   private static List<PsiFile> filterFilesTo(PsiFile[] files, List<PsiFile> list) {
166     for (PsiFile file : files) {
167       if (canBeFormatted(file)) {
168         list.add(file);
169       }
170     }
171     return list;
172   }
173
174   @Nullable
175   private FutureTask<Boolean> getPreviousProcessorTask(@NotNull PsiFile file, boolean processChangedTextOnly) {
176     return myPreviousCodeProcessor != null ? myPreviousCodeProcessor.preprocessFile(file, processChangedTextOnly)
177                                            : null;
178   }
179
180   public void addFileFilter(@NotNull FileFilter filter) {
181     myFilters.add(filter);
182   }
183
184   protected void setProcessChangedTextOnly(boolean value) {
185     myProcessChangedTextOnly = value;
186   }
187   /**
188    * Ensures that given file is ready to reformatting and prepares it if necessary.
189    *
190    * @param file                    file to process
191    * @param processChangedTextOnly  flag that defines is only the changed text (in terms of VCS change) should be processed
192    * @return          task that triggers formatting of the given file. Returns value of that task indicates whether formatting
193    *                  is finished correctly or not (exception occurred, user cancelled formatting etc)
194    * @throws IncorrectOperationException    if unexpected exception occurred during formatting
195    */
196   @NotNull
197   protected abstract FutureTask<Boolean> prepareTask(@NotNull PsiFile file, boolean processChangedTextOnly) throws IncorrectOperationException;
198
199   public FutureTask<Boolean> preprocessFile(@NotNull PsiFile file, boolean processChangedTextOnly) throws IncorrectOperationException {
200     final FutureTask<Boolean> previousTask = getPreviousProcessorTask(file, processChangedTextOnly);
201     final FutureTask<Boolean> currentTask = prepareTask(file, processChangedTextOnly);
202
203     return new FutureTask<Boolean>(new Callable<Boolean>() {
204       @Override
205       public Boolean call() throws Exception {
206         if (previousTask != null) {
207           previousTask.run();
208           if (!previousTask.get() || previousTask.isCancelled()) return false;
209         }
210
211         ApplicationManager.getApplication().runWriteAction(new Runnable() {
212           @Override
213           public void run() {
214             currentTask.run();
215           }
216         });
217
218         return currentTask.get() && !currentTask.isCancelled();
219       }
220     });
221   }
222
223   public void run() {
224     if (myFile != null) {
225       runProcessFile(myFile);
226       return;
227     }
228
229     FileTreeIterator iterator;
230     if (myFiles != null) {
231       iterator = new FileTreeIterator(myFiles);
232     }
233     else {
234       iterator = myProcessChangedTextOnly ? buildChangedFilesIterator()
235                                           : buildFileTreeIterator();
236     }
237     runProcessFiles(iterator);
238   }
239
240   private FileTreeIterator buildFileTreeIterator() {
241     if (myDirectory != null) {
242       return new FileTreeIterator(myDirectory);
243     }
244     else if (myFiles != null) {
245       return new FileTreeIterator(myFiles);
246     }
247     else if (myModule != null) {
248       return new FileTreeIterator(myModule);
249     }
250     else if (myProject != null) {
251       return new FileTreeIterator(myProject);
252     }
253
254     return new FileTreeIterator(Collections.<PsiFile>emptyList());
255   }
256
257   @NotNull
258   private FileTreeIterator buildChangedFilesIterator() {
259     List<PsiFile> files = getChangedFilesFromContext();
260     return new FileTreeIterator(files);
261   }
262
263   @NotNull
264   private List<PsiFile> getChangedFilesFromContext() {
265     List<PsiDirectory> dirs = getAllSearchableDirsFromContext();
266     return FormatChangedTextUtil.getChangedFilesFromDirs(myProject, dirs);
267   }
268
269   private List<PsiDirectory> getAllSearchableDirsFromContext() {
270     List<PsiDirectory> dirs = ContainerUtil.newArrayList();
271     if (myDirectory != null) {
272       dirs.add(myDirectory);
273     }
274     else if (myModule != null) {
275       List<PsiDirectory> allModuleDirs = FileTreeIterator.collectModuleDirectories(myModule);
276       dirs.addAll(allModuleDirs);
277     }
278     else if (myProject != null) {
279       List<PsiDirectory> allProjectDirs = FileTreeIterator.collectProjectDirectories(myProject);
280       dirs.addAll(allProjectDirs);
281     }
282     return dirs;
283   }
284
285
286   private void runProcessFile(@NotNull final PsiFile file) {
287     Document document = PsiDocumentManager.getInstance(myProject).getDocument(file);
288
289     if (document == null) {
290       return;
291     }
292
293     if (!FileDocumentManager.getInstance().requestWriting(document, myProject)) {
294       Messages.showMessageDialog(myProject, PsiBundle.message("cannot.modify.a.read.only.file", file.getName()),
295                                  CodeInsightBundle.message("error.dialog.readonly.file.title"),
296                                  Messages.getErrorIcon()
297       );
298       return;
299     }
300
301     final Runnable[] resultRunnable = new Runnable[1];
302     Runnable readAction = new Runnable() {
303       @Override
304       public void run() {
305         if (!checkFileWritable(file)) return;
306         try{
307           resultRunnable[0] = preprocessFile(file, myProcessChangedTextOnly);
308         }
309         catch(IncorrectOperationException e){
310           LOG.error(e);
311         }
312       }
313     };
314     Runnable writeAction = new Runnable() {
315       @Override
316       public void run() {
317         if (resultRunnable[0] != null) {
318           resultRunnable[0].run();
319         }
320       }
321     };
322     runLayoutCodeProcess(readAction, writeAction, false );
323   }
324
325   private boolean checkFileWritable(final PsiFile file){
326     if (!file.isWritable()){
327       MessagesEx.fileIsReadOnly(myProject, file.getVirtualFile())
328           .setTitle(CodeInsightBundle.message("error.dialog.readonly.file.title"))
329           .showLater();
330       return false;
331     }
332     else{
333       return true;
334     }
335   }
336
337   @Nullable
338   private Runnable createIterativeFileProcessor(@NotNull final FileTreeIterator fileIterator) {
339     return new Runnable() {
340       @Override
341       public void run() {
342         SequentialModalProgressTask progressTask = new SequentialModalProgressTask(myProject, myCommandName);
343         progressTask.setMinIterationTime(100);
344         ReformatFilesTask reformatFilesTask = new ReformatFilesTask(fileIterator);
345         reformatFilesTask.setCompositeTask(progressTask);
346         progressTask.setTask(reformatFilesTask);
347         ProgressManager.getInstance().run(progressTask);
348       }
349     };
350   }
351
352   private void runProcessFiles(@NotNull final FileTreeIterator fileIterator) {
353     final Runnable[] resultRunnable = new Runnable[1];
354
355     Runnable readAction = new Runnable() {
356       @Override
357       public void run() {
358         resultRunnable[0] = createIterativeFileProcessor(fileIterator);
359       }
360     };
361
362     Runnable writeAction = new Runnable() {
363       @Override
364       public void run() {
365         if (resultRunnable[0] != null) {
366           resultRunnable[0].run();
367         }
368       }
369     };
370
371     runLayoutCodeProcess(readAction, writeAction, true);
372   }
373
374   private static boolean canBeFormatted(PsiFile file) {
375     if (LanguageFormatting.INSTANCE.forContext(file) == null) {
376       return false;
377     }
378     VirtualFile virtualFile = file.getVirtualFile();
379     if (virtualFile == null) return true;
380
381     if (ProjectCoreUtil.isProjectOrWorkspaceFile(virtualFile)) return false;
382
383     for (GeneratedSourcesFilter filter : GeneratedSourcesFilter.EP_NAME.getExtensions()) {
384       if (filter.isGeneratedSource(virtualFile, file.getProject())) {
385         return false;
386       }
387     }
388     return true;
389   }
390
391   private void runLayoutCodeProcess(final Runnable readAction, final Runnable writeAction, final boolean globalAction) {
392     final ProgressWindow progressWindow = new ProgressWindow(true, myProject);
393     progressWindow.setTitle(myCommandName);
394     progressWindow.setText(myProgressText);
395
396     final ModalityState modalityState = ModalityState.current();
397
398     final Runnable process = new Runnable() {
399       @Override
400       public void run() {
401         ApplicationManager.getApplication().runReadAction(readAction);
402       }
403     };
404
405     Runnable runnable = new Runnable() {
406       @Override
407       public void run() {
408         try {
409           ProgressManager.getInstance().runProcess(process, progressWindow);
410         }
411         catch(ProcessCanceledException e) {
412           return;
413         }
414         catch(IndexNotReadyException e) {
415           return;
416         }
417
418         final Runnable writeRunnable = new Runnable() {
419           @Override
420           public void run() {
421             CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
422               @Override
423               public void run() {
424                 if (globalAction) CommandProcessor.getInstance().markCurrentCommandAsGlobal(myProject);
425                 try {
426                   writeAction.run();
427
428                   if (myPostRunnable != null) {
429                     ApplicationManager.getApplication().invokeLater(myPostRunnable);
430                   }
431                 }
432                 catch (IndexNotReadyException ignored) {
433                 }
434               }
435             }, myCommandName, null);
436           }
437         };
438
439         if (ApplicationManager.getApplication().isUnitTestMode()) {
440           writeRunnable.run();
441         }
442         else {
443           ApplicationManager.getApplication().invokeLater(writeRunnable, modalityState, myProject.getDisposed());
444         }
445       }
446     };
447
448     if (ApplicationManager.getApplication().isUnitTestMode()) {
449       runnable.run();
450     }
451     else {
452       ApplicationManager.getApplication().executeOnPooledThread(runnable);
453     }
454   }
455
456   public void runWithoutProgress() throws IncorrectOperationException {
457     final Runnable runnable = preprocessFile(myFile, myProcessChangedTextOnly);
458     runnable.run();
459   }
460
461   private class ReformatFilesTask implements SequentialTask {
462     private SequentialModalProgressTask myCompositeTask;
463
464     private final FileTreeIterator myFileTreeIterator;
465     private final FileTreeIterator myCountingIterator;
466
467     private int myTotalFiles = 0;
468     private int myFilesProcessed = 0;
469     private boolean myStopFormatting;
470     private boolean myFilesCountingFinished;
471
472     ReformatFilesTask(@NotNull FileTreeIterator fileIterator) {
473       myFileTreeIterator = fileIterator;
474       myCountingIterator = new FileTreeIterator(fileIterator);
475     }
476
477     @Override
478     public void prepare() {
479     }
480
481     @Override
482     public boolean isDone() {
483       return myStopFormatting || !myFileTreeIterator.hasNext();
484     }
485
486     private void countingIteration() {
487       if (myCountingIterator.hasNext()) {
488         myCountingIterator.next();
489         myTotalFiles++;
490       }
491       else {
492         myFilesCountingFinished = true;
493       }
494     }
495
496     @Override
497     public boolean iteration() {
498       if (myStopFormatting) {
499         return true;
500       }
501
502       if (!myFilesCountingFinished) {
503         updateIndicatorText(ApplicationBundle.message("bulk.reformat.prepare.progress.text"), "");
504         countingIteration();
505         return true;
506       }
507
508       updateIndicatorFraction(myFilesProcessed);
509
510       if (myFileTreeIterator.hasNext()) {
511         final PsiFile file = myFileTreeIterator.next();
512         myFilesProcessed++;
513         if (file.isWritable() && canBeFormatted(file) && acceptedByFilters(file)) {
514           updateIndicatorText(ApplicationBundle.message("bulk.reformat.process.progress.text"), getPresentablePath(file));
515           ApplicationManager.getApplication().runWriteAction(new Runnable() {
516             @Override
517             public void run() {
518               performFileProcessing(file);
519             }
520           });
521         }
522       }
523
524       return true;
525     }
526
527     private void performFileProcessing(@NotNull PsiFile file) {
528       FutureTask<Boolean> task = preprocessFile(file, myProcessChangedTextOnly);
529       task.run();
530       try {
531         if (!task.get() || task.isCancelled()) {
532           myStopFormatting = true;
533         }
534       }
535       catch (InterruptedException e) {
536         LOG.error("Got unexpected exception during formatting", e);
537       }
538       catch (ExecutionException e) {
539         LOG.error("Got unexpected exception during formatting", e);
540       }
541     }
542
543     private void updateIndicatorText(@NotNull String upperLabel, @NotNull String downLabel) {
544       ProgressIndicator indicator = myCompositeTask.getIndicator();
545       if (indicator != null) {
546         indicator.setText(upperLabel);
547         indicator.setText2(downLabel);
548       }
549     }
550
551     private String getPresentablePath(@NotNull PsiFile file) {
552       VirtualFile vFile = file.getVirtualFile();
553       return vFile != null ? ProjectUtil.calcRelativeToProjectPath(vFile, myProject) : file.getName();
554     }
555
556     private void updateIndicatorFraction(int processed) {
557       ProgressIndicator indicator = myCompositeTask.getIndicator();
558       if (indicator != null) {
559         indicator.setFraction((double)processed / myTotalFiles);
560       }
561     }
562
563     @Override
564     public void stop() {
565       myStopFormatting = true;
566     }
567
568     public void setCompositeTask(@Nullable SequentialModalProgressTask compositeTask) {
569       myCompositeTask = compositeTask;
570     }
571   }
572
573   private boolean acceptedByFilters(@NotNull PsiFile file) {
574     VirtualFile vFile = file.getVirtualFile();
575     if (vFile == null) {
576       return false;
577     }
578
579     for (FileFilter filter : myFilters) {
580       if (!filter.accept(file.getVirtualFile())) {
581         return false;
582       }
583     }
584
585     return true;
586   }
587 }