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