af96331aa30653fa46b6a40c291218043a817b42
[idea/community.git] / platform / lang-impl / src / com / intellij / psi / impl / source / PostprocessReformattingAspect.java
1 /*
2  * Copyright 2000-2013 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 package com.intellij.psi.impl.source;
17
18 import com.intellij.formatting.FormatTextRanges;
19 import com.intellij.lang.ASTNode;
20 import com.intellij.lang.injection.InjectedLanguageManager;
21 import com.intellij.openapi.Disposable;
22 import com.intellij.openapi.application.Application;
23 import com.intellij.openapi.application.ApplicationAdapter;
24 import com.intellij.openapi.application.ApplicationListener;
25 import com.intellij.openapi.application.ApplicationManager;
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.RangeMarker;
30 import com.intellij.openapi.fileTypes.FileTypeManager;
31 import com.intellij.openapi.progress.ProgressManager;
32 import com.intellij.openapi.project.Project;
33 import com.intellij.openapi.util.*;
34 import com.intellij.openapi.vfs.VirtualFile;
35 import com.intellij.pom.PomManager;
36 import com.intellij.pom.PomModelAspect;
37 import com.intellij.pom.event.PomModelEvent;
38 import com.intellij.pom.tree.TreeAspect;
39 import com.intellij.pom.tree.events.ChangeInfo;
40 import com.intellij.pom.tree.events.TreeChange;
41 import com.intellij.pom.tree.events.TreeChangeEvent;
42 import com.intellij.psi.*;
43 import com.intellij.psi.codeStyle.CodeStyleSettings;
44 import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
45 import com.intellij.psi.impl.PsiTreeDebugBuilder;
46 import com.intellij.psi.impl.source.codeStyle.CodeEditUtil;
47 import com.intellij.psi.impl.source.codeStyle.CodeFormatterFacade;
48 import com.intellij.psi.impl.source.codeStyle.IndentHelperImpl;
49 import com.intellij.psi.impl.source.tree.*;
50 import com.intellij.util.LocalTimeCounter;
51 import com.intellij.util.containers.ContainerUtilRt;
52 import com.intellij.util.text.CharArrayUtil;
53 import org.jetbrains.annotations.NotNull;
54 import org.jetbrains.annotations.TestOnly;
55
56 import java.util.*;
57 import java.util.concurrent.atomic.AtomicInteger;
58
59 public class PostprocessReformattingAspect implements PomModelAspect {
60   private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.PostprocessReformattingAspect");
61   private final Project myProject;
62   private final PsiManager myPsiManager;
63   private final TreeAspect myTreeAspect;
64   private final Map<FileViewProvider, List<ASTNode>> myReformatElements = new HashMap<FileViewProvider, List<ASTNode>>();
65   private volatile int myDisabledCounter = 0;
66   private final Set<FileViewProvider> myUpdatedProviders = new HashSet<FileViewProvider>();
67   private final AtomicInteger myPostponedCounter = new AtomicInteger();
68
69   public PostprocessReformattingAspect(Project project, PsiManager psiManager, TreeAspect treeAspect,final CommandProcessor processor) {
70     myProject = project;
71     myPsiManager = psiManager;
72     myTreeAspect = treeAspect;
73     PomManager.getModel(psiManager.getProject())
74       .registerAspect(PostprocessReformattingAspect.class, this, Collections.singleton((PomModelAspect)treeAspect));
75
76     ApplicationListener applicationListener = new ApplicationAdapter() {
77       @Override
78       public void writeActionStarted(final Object action) {
79         if (processor != null) {
80           final Project project = processor.getCurrentCommandProject();
81           if (project == myProject) {
82             incrementPostponedCounter();
83           }
84         }
85       }
86
87       @Override
88       public void writeActionFinished(final Object action) {
89         if (processor != null) {
90           final Project project = processor.getCurrentCommandProject();
91           if (project == myProject) {
92             decrementPostponedCounter();
93           }
94         }
95       }
96     };
97     ApplicationManager.getApplication().addApplicationListener(applicationListener, project);
98   }
99
100   public void disablePostprocessFormattingInside(final Runnable runnable) {
101     disablePostprocessFormattingInside(new NullableComputable<Object>() {
102       @Override
103       public Object compute() {
104         runnable.run();
105         return null;
106       }
107     });
108   }
109
110   public <T> T disablePostprocessFormattingInside(Computable<T> computable) {
111     try {
112       myDisabledCounter++;
113       return computable.compute();
114     }
115     finally {
116       myDisabledCounter--;
117       LOG.assertTrue(myDisabledCounter > 0 || !isDisabled());
118     }
119   }
120
121   public void postponeFormattingInside(final Runnable runnable) {
122     postponeFormattingInside(new NullableComputable<Object>() {
123       @Override
124       public Object compute() {
125         runnable.run();
126         return null;
127       }
128     });
129   }
130
131   public <T> T postponeFormattingInside(Computable<T> computable) {
132     Application application = ApplicationManager.getApplication();
133     application.assertIsDispatchThread();
134     try {
135       incrementPostponedCounter();
136       return computable.compute();
137     }
138     finally {
139       decrementPostponedCounter();
140     }
141   }
142
143   private void incrementPostponedCounter() {
144     myPostponedCounter.incrementAndGet();
145   }
146
147   private void decrementPostponedCounter() {
148     Application application = ApplicationManager.getApplication();
149     application.assertIsDispatchThread();
150     if (myPostponedCounter.decrementAndGet() == 0) {
151       if (application.isWriteAccessAllowed()) {
152         doPostponedFormatting();
153       }
154       else {
155         application.runWriteAction(new Runnable() {
156           @Override
157           public void run() {
158             doPostponedFormatting();
159           }
160         });
161       }
162     }
163   }
164
165   private static void atomic(@NotNull Runnable r) {
166     ProgressManager.getInstance().executeNonCancelableSection(r);
167   }
168
169   @Override
170   public void update(final PomModelEvent event) {
171     atomic(new Runnable() {
172       @Override
173       public void run() {
174         if (isDisabled() || myPostponedCounter.get() == 0 && !ApplicationManager.getApplication().isUnitTestMode()) return;
175         final TreeChangeEvent changeSet = (TreeChangeEvent)event.getChangeSet(myTreeAspect);
176         if (changeSet == null) return;
177         final PsiElement psiElement = changeSet.getRootElement().getPsi();
178         if (psiElement == null) return;
179         PsiFile containingFile = InjectedLanguageManager.getInstance(psiElement.getProject()).getTopLevelFile(psiElement);
180         final FileViewProvider viewProvider = containingFile.getViewProvider();
181
182         if (!viewProvider.isEventSystemEnabled()) return;
183         myUpdatedProviders.add(viewProvider);
184         for (final ASTNode node : changeSet.getChangedElements()) {
185           final TreeChange treeChange = changeSet.getChangesByElement(node);
186           for (final ASTNode affectedChild : treeChange.getAffectedChildren()) {
187             final ChangeInfo childChange = treeChange.getChangeByChild(affectedChild);
188             switch (childChange.getChangeType()) {
189               case ChangeInfo.ADD:
190               case ChangeInfo.REPLACE:
191                 postponeFormatting(viewProvider, affectedChild);
192                 break;
193               case ChangeInfo.CONTENTS_CHANGED:
194                 if (!CodeEditUtil.isNodeGenerated(affectedChild)) {
195                   ((TreeElement)affectedChild).acceptTree(new RecursiveTreeElementWalkingVisitor() {
196                     @Override
197                     protected void visitNode(TreeElement element) {
198                       if (CodeEditUtil.isNodeGenerated(element) && CodeEditUtil.isSuspendedNodesReformattingAllowed()) {
199                         postponeFormatting(viewProvider, element);
200                         return;
201                       }
202                       super.visitNode(element);
203                     }
204                   });
205                 }
206                 break;
207             }
208           }
209         }
210       }
211     });
212   }
213
214   public void doPostponedFormatting() {
215     atomic(new Runnable() {
216       @Override
217       public void run() {
218         if (isDisabled()) return;
219         try {
220           FileViewProvider[] viewProviders = myUpdatedProviders.toArray(new FileViewProvider[myUpdatedProviders.size()]);
221           for (final FileViewProvider viewProvider : viewProviders) {
222             doPostponedFormatting(viewProvider);
223           }
224         }
225         catch (Exception e) {
226           LOG.error(e);
227         }
228         finally {
229           LOG.assertTrue(myReformatElements.isEmpty(), myReformatElements);
230         }
231       }
232     });
233   }
234
235   public void postponedFormatting(final FileViewProvider viewProvider) {
236     postponedFormattingImpl(viewProvider, true);
237   }
238
239   public void doPostponedFormatting(final FileViewProvider viewProvider) {
240     postponedFormattingImpl(viewProvider, false);
241   }
242
243   private void postponedFormattingImpl(final FileViewProvider viewProvider, final boolean check) {
244     atomic(new Runnable() {
245       @Override
246       public void run() {
247         if (isDisabled() || check && !myUpdatedProviders.contains(viewProvider)) return;
248
249         try {
250           disablePostprocessFormattingInside(new Runnable() {
251             @Override
252             public void run() {
253               doPostponedFormattingInner(viewProvider);
254             }
255           });
256         }
257         finally {
258           myUpdatedProviders.remove(viewProvider);
259           myReformatElements.remove(viewProvider);
260         }
261       }
262     });
263   }
264
265   public boolean isViewProviderLocked(final FileViewProvider fileViewProvider) {
266     return myReformatElements.containsKey(fileViewProvider);
267   }
268
269   public void beforeDocumentChanged(FileViewProvider viewProvider) {
270     if (isViewProviderLocked(viewProvider)) {
271       throw new RuntimeException("Document is locked by write PSI operations. " +
272                                  "Use PsiDocumentManager.doPostponedOperationsAndUnblockDocument() to commit PSI changes to the document.");
273     }
274     postponedFormatting(viewProvider);
275   }
276
277   public static PostprocessReformattingAspect getInstance(Project project) {
278     return project.getComponent(PostprocessReformattingAspect.class);
279   }
280
281   private void postponeFormatting(final FileViewProvider viewProvider, final ASTNode child) {
282     if (!CodeEditUtil.isNodeGenerated(child) && child.getElementType() != TokenType.WHITE_SPACE) {
283       final int oldIndent = CodeEditUtil.getOldIndentation(child);
284       LOG.assertTrue(oldIndent >= 0,
285                      "for not generated items old indentation must be defined: element=" + child + ", text=" + child.getText());
286     }
287     List<ASTNode> list = myReformatElements.get(viewProvider);
288     if (list == null) {
289       list = new ArrayList<ASTNode>();
290       myReformatElements.put(viewProvider, list);
291     }
292     list.add(child);
293   }
294
295   private void doPostponedFormattingInner(final FileViewProvider key) {
296     final List<ASTNode> astNodes = myReformatElements.remove(key);
297     final Document document = key.getDocument();
298     // Sort ranges by end offsets so that we won't need any offset adjustment after reformat or reindent
299     if (document == null) return;
300
301     final VirtualFile virtualFile = key.getVirtualFile();
302     if (!virtualFile.isValid()) return;
303
304     final TreeSet<PostprocessFormattingTask> postProcessTasks = new TreeSet<PostprocessFormattingTask>();
305     Collection<Disposable> toDispose = ContainerUtilRt.newArrayList();
306     try {
307       // process all roots in viewProvider to find marked for reformat before elements and create appropriate range markers
308       handleReformatMarkers(key, postProcessTasks);
309       toDispose.addAll(postProcessTasks);
310
311       // then we create ranges by changed nodes. One per node. There ranges can intersect. Ranges are sorted by end offset.
312       if (astNodes != null) createActionsMap(astNodes, key, postProcessTasks);
313
314       if (Boolean.getBoolean("check.psi.is.valid") && ApplicationManager.getApplication().isUnitTestMode()) {
315         checkPsiIsCorrect(key);
316       }
317
318       while (!postProcessTasks.isEmpty()) {
319         // now we have to normalize actions so that they not intersect and ordered in most appropriate way
320         // (free reformatting -> reindent -> formatting under reindent)
321         final List<PostponedAction> normalizedActions = normalizeAndReorderPostponedActions(postProcessTasks, document);
322         toDispose.addAll(normalizedActions);
323
324         // only in following loop real changes in document are made
325         for (final PostponedAction normalizedAction : normalizedActions) {
326           CodeStyleSettings settings = CodeStyleSettingsManager.getSettings(myPsiManager.getProject());
327           boolean old = settings.ENABLE_JAVADOC_FORMATTING;
328           settings.ENABLE_JAVADOC_FORMATTING = false;
329           try {
330             normalizedAction.execute(key);
331           }
332           finally {
333             settings.ENABLE_JAVADOC_FORMATTING = old;
334           }
335         }
336       }
337     }
338     finally {
339       for (Disposable disposable : toDispose) {
340         //noinspection SSBasedInspection
341         disposable.dispose();
342       }
343     }
344   }
345
346   private void checkPsiIsCorrect(final FileViewProvider key) {
347     PsiFile actualPsi = key.getPsi(key.getBaseLanguage());
348
349     PsiTreeDebugBuilder treeDebugBuilder = new PsiTreeDebugBuilder().setShowErrorElements(false).setShowWhiteSpaces(false);
350
351     String actualPsiTree = treeDebugBuilder.psiToString(actualPsi);
352
353     String fileName = key.getVirtualFile().getName();
354     PsiFile psi = PsiFileFactory.getInstance(myProject)
355       .createFileFromText(fileName, FileTypeManager.getInstance().getFileTypeByFileName(fileName), actualPsi.getNode().getText(),
356                           LocalTimeCounter.currentTime(), false);
357
358     if (actualPsi.getClass().equals(psi.getClass())) {
359       String expectedPsi = treeDebugBuilder.psiToString(psi);
360
361       if (!expectedPsi.equals(actualPsiTree)) {
362         myReformatElements.clear();
363         assert expectedPsi.equals(actualPsiTree) : "Refactored psi should be the same as result of parsing";
364       }
365     }
366   }
367
368   private List<PostponedAction> normalizeAndReorderPostponedActions(TreeSet<PostprocessFormattingTask> rangesToProcess, Document document) {
369     final List<PostprocessFormattingTask> freeFormattingActions = new ArrayList<PostprocessFormattingTask>();
370     final List<ReindentTask> indentActions = new ArrayList<ReindentTask>();
371
372     PostprocessFormattingTask accumulatedTask = null;
373     Iterator<PostprocessFormattingTask> iterator = rangesToProcess.iterator();
374     while (iterator.hasNext()) {
375       final PostprocessFormattingTask currentTask = iterator.next();
376       if (accumulatedTask == null) {
377         accumulatedTask = currentTask;
378         iterator.remove();
379       }
380       else if (accumulatedTask.getStartOffset() > currentTask.getEndOffset() ||
381                accumulatedTask.getStartOffset() == currentTask.getEndOffset() &&
382                 !canStickActionsTogether(accumulatedTask, currentTask)) {
383         // action can be pushed
384         if (accumulatedTask instanceof ReindentTask) {
385           indentActions.add((ReindentTask) accumulatedTask);
386         }
387         else {
388           freeFormattingActions.add(accumulatedTask);
389         }
390
391         accumulatedTask = currentTask;
392         iterator.remove();
393       }
394       else if (accumulatedTask instanceof ReformatTask && currentTask instanceof ReindentTask) {
395         // split accumulated reformat range into two
396         if (accumulatedTask.getStartOffset() < currentTask.getStartOffset()) {
397           final RangeMarker endOfRange = document.createRangeMarker(accumulatedTask.getStartOffset(), currentTask.getStartOffset());
398           // add heading reformat part
399           rangesToProcess.add(new ReformatTask(endOfRange));
400           // and manage heading whitespace because formatter does not edit it in previous action
401           iterator = rangesToProcess.iterator();
402           //noinspection StatementWithEmptyBody
403           while (iterator.next().getRange() != currentTask.getRange()) ;
404         }
405         final RangeMarker rangeToProcess = document.createRangeMarker(currentTask.getEndOffset(), accumulatedTask.getEndOffset());
406         freeFormattingActions.add(new ReformatWithHeadingWhitespaceTask(rangeToProcess));
407         accumulatedTask = currentTask;
408         iterator.remove();
409       }
410       else {
411         if (!(accumulatedTask instanceof ReindentTask)) {
412           iterator.remove();
413
414           boolean withLeadingWhitespace = accumulatedTask instanceof ReformatWithHeadingWhitespaceTask;
415           if (accumulatedTask instanceof ReformatTask &&
416               currentTask instanceof ReformatWithHeadingWhitespaceTask &&
417               accumulatedTask.getStartOffset() == currentTask.getStartOffset()) {
418             withLeadingWhitespace = true;
419           }
420           else if (accumulatedTask instanceof ReformatWithHeadingWhitespaceTask &&
421               currentTask instanceof ReformatTask &&
422               accumulatedTask.getStartOffset() < currentTask.getStartOffset()) {
423             withLeadingWhitespace = false;
424           }
425           int newStart = Math.min(accumulatedTask.getStartOffset(), currentTask.getStartOffset());
426           int newEnd = Math.max(accumulatedTask.getEndOffset(), currentTask.getEndOffset());
427           RangeMarker rangeMarker;
428
429           if (accumulatedTask.getStartOffset() == newStart && accumulatedTask.getEndOffset() == newEnd) {
430             rangeMarker = accumulatedTask.getRange();
431           }
432           else if (currentTask.getStartOffset() == newStart && currentTask.getEndOffset() == newEnd) {
433             rangeMarker = currentTask.getRange();
434           }
435           else {
436             rangeMarker = document.createRangeMarker(newStart, newEnd);
437           }
438
439           if (withLeadingWhitespace) {
440             accumulatedTask = new ReformatWithHeadingWhitespaceTask(rangeMarker);
441           }
442           else {
443             accumulatedTask = new ReformatTask(rangeMarker);
444
445           }
446         }
447         else if (currentTask instanceof ReindentTask) {
448           iterator.remove();
449         } // TODO[ik]: need to be fixed to correctly process indent inside indent
450       }
451     }
452     if (accumulatedTask != null) {
453       if (accumulatedTask instanceof ReindentTask) {
454         indentActions.add((ReindentTask) accumulatedTask);
455       }
456       else {
457         freeFormattingActions.add(accumulatedTask);
458       }
459     }
460
461     final List<PostponedAction> result = new ArrayList<PostponedAction>();
462     Collections.reverse(freeFormattingActions);
463     Collections.reverse(indentActions);
464
465     if (!freeFormattingActions.isEmpty()) {
466       FormatTextRanges ranges = new FormatTextRanges();
467       for (PostprocessFormattingTask action : freeFormattingActions) {
468         TextRange range = TextRange.create(action);
469         ranges.add(range, action instanceof ReformatWithHeadingWhitespaceTask);
470       }
471       result.add(new ReformatRangesAction(ranges));
472     }
473
474     if (!indentActions.isEmpty()) {
475       ReindentRangesAction reindentRangesAction = new ReindentRangesAction();
476       for (ReindentTask action : indentActions) {
477         reindentRangesAction.add(action.getRange(), action.getOldIndent());
478       }
479       result.add(reindentRangesAction);
480     }
481
482     return result;
483   }
484
485   private static boolean canStickActionsTogether(final PostprocessFormattingTask currentTask,
486                                                  final PostprocessFormattingTask nextTask) {
487     // empty reformat markers can't be stuck together with any action
488     if (nextTask instanceof ReformatWithHeadingWhitespaceTask && nextTask.getStartOffset() == nextTask.getEndOffset()) return false;
489     if (currentTask instanceof ReformatWithHeadingWhitespaceTask && currentTask.getStartOffset() == currentTask.getEndOffset()) {
490       return false;
491     }
492     // reindent actions can't be be stuck at all
493     return !(currentTask instanceof ReindentTask);
494   }
495
496   private static void createActionsMap(final List<ASTNode> astNodes,
497                                        final FileViewProvider provider,
498                                        final TreeSet<PostprocessFormattingTask> rangesToProcess) {
499     final Set<ASTNode> nodesToProcess = new HashSet<ASTNode>(astNodes);
500     final Document document = provider.getDocument();
501     if (document == null) {
502       return;
503     }
504     for (final ASTNode node : astNodes) {
505       nodesToProcess.remove(node);
506       final FileElement fileElement = TreeUtil.getFileElement((TreeElement)node);
507       if (fileElement == null || ((PsiFile)fileElement.getPsi()).getViewProvider() != provider) continue;
508       final boolean isGenerated = CodeEditUtil.isNodeGenerated(node);
509
510       ((TreeElement)node).acceptTree(new RecursiveTreeElementVisitor() {
511         boolean inGeneratedContext = !isGenerated;
512
513         @Override
514         protected boolean visitNode(TreeElement element) {
515           if (nodesToProcess.contains(element)) return false;
516
517           final boolean currentNodeGenerated = CodeEditUtil.isNodeGenerated(element);
518           CodeEditUtil.setNodeGenerated(element, false);
519           if (currentNodeGenerated && !inGeneratedContext) {
520             rangesToProcess.add(new ReformatTask(document.createRangeMarker(element.getTextRange())));
521             inGeneratedContext = true;
522           }
523           if (!currentNodeGenerated && inGeneratedContext) {
524             if (element.getElementType() == TokenType.WHITE_SPACE) return false;
525             final int oldIndent = CodeEditUtil.getOldIndentation(element);
526             CodeEditUtil.setOldIndentation(element, -1);
527             LOG.assertTrue(oldIndent >= 0, "for not generated items old indentation must be defined: element " + element);
528             rangesToProcess.add(new ReindentTask(document.createRangeMarker(element.getTextRange()), oldIndent));
529             inGeneratedContext = false;
530           }
531           return true;
532         }
533
534         @Override
535         public void visitComposite(CompositeElement composite) {
536           boolean oldGeneratedContext = inGeneratedContext;
537           super.visitComposite(composite);
538           inGeneratedContext = oldGeneratedContext;
539         }
540
541         @Override
542         public void visitLeaf(LeafElement leaf) {
543           boolean oldGeneratedContext = inGeneratedContext;
544           super.visitLeaf(leaf);
545           inGeneratedContext = oldGeneratedContext;
546         }
547       });
548     }
549   }
550
551   private static void handleReformatMarkers(final FileViewProvider key, final TreeSet<PostprocessFormattingTask> rangesToProcess) {
552     final Document document = key.getDocument();
553     if (document == null) {
554       return;
555     }
556     for (final FileElement fileElement : ((SingleRootFileViewProvider)key).getKnownTreeRoots()) {
557       fileElement.acceptTree(new RecursiveTreeElementWalkingVisitor() {
558         @Override
559         protected void visitNode(TreeElement element) {
560           if (CodeEditUtil.isMarkedToReformatBefore(element)) {
561             CodeEditUtil.markToReformatBefore(element, false);
562             rangesToProcess.add(new ReformatWithHeadingWhitespaceTask(
563               document.createRangeMarker(element.getStartOffset(), element.getStartOffset()))
564             );
565           }
566           else if (CodeEditUtil.isMarkedToReformat(element)) {
567             CodeEditUtil.markToReformat(element, false);
568             rangesToProcess.add(new ReformatWithHeadingWhitespaceTask(
569               document.createRangeMarker(element.getStartOffset(), element.getStartOffset() + element.getTextLength()))
570             );
571           }
572           super.visitNode(element);
573         }
574       });
575     }
576   }
577
578   private static void adjustIndentationInRange(final PsiFile file,
579                                                final Document document,
580                                                final TextRange[] indents,
581                                                final int indentAdjustment) {
582     final CharSequence charsSequence = document.getCharsSequence();
583     for (final TextRange indent : indents) {
584       final String oldIndentStr = charsSequence.subSequence(indent.getStartOffset() + 1, indent.getEndOffset()).toString();
585       final int oldIndent = IndentHelperImpl.getIndent(file.getProject(), file.getFileType(), oldIndentStr, true);
586       final String newIndentStr = IndentHelperImpl
587         .fillIndent(file.getProject(), file.getFileType(), Math.max(oldIndent + indentAdjustment, 0));
588       document.replaceString(indent.getStartOffset() + 1, indent.getEndOffset(), newIndentStr);
589     }
590   }
591
592   @SuppressWarnings("StatementWithEmptyBody")
593   private static int getNewIndent(final PsiFile psiFile, final int firstWhitespace) {
594     final Document document = psiFile.getViewProvider().getDocument();
595     assert document != null;
596     final int startOffset = document.getLineStartOffset(document.getLineNumber(firstWhitespace));
597     int endOffset = startOffset;
598     final CharSequence charsSequence = document.getCharsSequence();
599     while (Character.isWhitespace(charsSequence.charAt(endOffset++))) ;
600     final String newIndentStr = charsSequence.subSequence(startOffset, endOffset - 1).toString();
601     return IndentHelperImpl.getIndent(psiFile.getProject(), psiFile.getFileType(), newIndentStr, true);
602   }
603
604   public boolean isDisabled() {
605     return myDisabledCounter > 0;
606   }
607
608   private CodeFormatterFacade getFormatterFacade(final FileViewProvider viewProvider) {
609     final CodeStyleSettings styleSettings = CodeStyleSettingsManager.getSettings(myPsiManager.getProject());
610     final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myPsiManager.getProject());
611     final Document document = viewProvider.getDocument();
612     assert document != null;
613     final CodeFormatterFacade codeFormatter = new CodeFormatterFacade(styleSettings);
614
615     documentManager.commitDocument(document);
616     return codeFormatter;
617   }
618
619   private abstract static class PostprocessFormattingTask implements Comparable<PostprocessFormattingTask>, Segment, Disposable {
620     @NotNull private final RangeMarker myRange;
621
622     public PostprocessFormattingTask(@NotNull RangeMarker rangeMarker) {
623       myRange = rangeMarker;
624     }
625
626     @Override
627     public int compareTo(@NotNull PostprocessFormattingTask o) {
628       RangeMarker o1 = myRange;
629       RangeMarker o2 = o.myRange;
630       if (o1.equals(o2)) return 0;
631       final int diff = o2.getEndOffset() - o1.getEndOffset();
632       if (diff == 0) {
633         if (o1.getStartOffset() == o2.getStartOffset()) return 0;
634         if (o1.getStartOffset() == o1.getEndOffset()) return -1; // empty ranges first
635         if (o2.getStartOffset() == o2.getEndOffset()) return 1; // empty ranges first
636         return o1.getStartOffset() - o2.getStartOffset();
637       }
638       return diff;
639     }
640
641     @NotNull
642     public RangeMarker getRange() {
643       return myRange;
644     }
645
646     @Override
647     public int getStartOffset() {
648       return myRange.getStartOffset();
649     }
650
651     @Override
652     public int getEndOffset() {
653       return myRange.getEndOffset();
654     }
655
656     @Override
657     public void dispose() {
658       if (myRange.isValid()) {
659         myRange.dispose();
660       }
661     }
662   }
663
664   private static class ReformatTask extends PostprocessFormattingTask {
665     public ReformatTask(RangeMarker rangeMarker) {
666       super(rangeMarker);
667     }
668   }
669
670   private static class ReformatWithHeadingWhitespaceTask extends PostprocessFormattingTask {
671     public ReformatWithHeadingWhitespaceTask(RangeMarker rangeMarker) {
672       super(rangeMarker);
673     }
674   }
675
676   private static class ReindentTask extends PostprocessFormattingTask {
677     private final int myOldIndent;
678
679     public ReindentTask(RangeMarker rangeMarker, int oldIndent) {
680       super(rangeMarker);
681       myOldIndent = oldIndent;
682     }
683
684     public int getOldIndent() {
685       return myOldIndent;
686     }
687   }
688
689   private interface PostponedAction extends Disposable {
690     void execute(FileViewProvider viewProvider);
691   }
692
693   private class ReformatRangesAction implements PostponedAction {
694     private final FormatTextRanges myRanges;
695
696     public ReformatRangesAction(FormatTextRanges ranges) {
697       myRanges = ranges;
698     }
699
700     @Override
701     public void execute(FileViewProvider viewProvider) {
702       final CodeFormatterFacade codeFormatter = getFormatterFacade(viewProvider);
703       codeFormatter.processText(viewProvider.getPsi(viewProvider.getBaseLanguage()), myRanges.ensureNonEmpty(), false);
704     }
705
706     @Override
707     public void dispose() {
708     }
709   }
710
711   private static class ReindentRangesAction implements PostponedAction {
712     private final List<Pair<Integer, RangeMarker>> myRangesToReindent = new ArrayList<Pair<Integer, RangeMarker>>();
713
714     public void add(RangeMarker rangeMarker, int oldIndent) {
715       myRangesToReindent.add(new Pair<Integer, RangeMarker>(oldIndent, rangeMarker));
716     }
717
718     @Override
719     public void execute(FileViewProvider viewProvider) {
720       final Document document = viewProvider.getDocument();
721       assert document != null;
722       final PsiFile psiFile = viewProvider.getPsi(viewProvider.getBaseLanguage());
723       for (Pair<Integer, RangeMarker> integerRangeMarkerPair : myRangesToReindent) {
724         RangeMarker marker = integerRangeMarkerPair.second;
725         final CharSequence charsSequence = document.getCharsSequence().subSequence(marker.getStartOffset(), marker.getEndOffset());
726         final int oldIndent = integerRangeMarkerPair.first;
727         final TextRange[] whitespaces = CharArrayUtil.getIndents(charsSequence, marker.getStartOffset());
728         final int indentAdjustment = getNewIndent(psiFile, marker.getStartOffset()) - oldIndent;
729         if (indentAdjustment != 0) adjustIndentationInRange(psiFile, document, whitespaces, indentAdjustment);
730       }
731     }
732
733     @Override
734     public void dispose() {
735       for (Pair<Integer, RangeMarker> pair : myRangesToReindent) {
736         RangeMarker marker = pair.second;
737         if (marker.isValid()) {
738           marker.dispose();
739         }
740       }
741     }
742   }
743
744   @TestOnly
745   public void clear() {
746     myReformatElements.clear();
747   }
748 }