Fix OC-3040 TODO[ik] implementation: need to be fixed to correctly process indent...
[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       }
379       else if (accumulatedTask.getStartOffset() > currentTask.getEndOffset() ||
380                accumulatedTask.getStartOffset() == currentTask.getEndOffset() &&
381                 !canStickActionsTogether(accumulatedTask, currentTask)) {
382         // action can be pushed
383         if (accumulatedTask instanceof ReindentTask) {
384           indentActions.add((ReindentTask) accumulatedTask);
385         }
386         else {
387           freeFormattingActions.add(accumulatedTask);
388         }
389
390         accumulatedTask = currentTask;
391       }
392       else if (accumulatedTask instanceof ReformatTask && currentTask instanceof ReindentTask) {
393         // split accumulated reformat range into two
394         if (accumulatedTask.getStartOffset() < currentTask.getStartOffset()) {
395           final RangeMarker endOfRange = document.createRangeMarker(accumulatedTask.getStartOffset(), currentTask.getStartOffset());
396           // add heading reformat part
397           rangesToProcess.add(new ReformatTask(endOfRange));
398           // and manage heading whitespace because formatter does not edit it in previous action
399           iterator = rangesToProcess.iterator();
400           //noinspection StatementWithEmptyBody
401           while (iterator.next().getRange() != currentTask.getRange()) ;
402         }
403         final RangeMarker rangeToProcess = document.createRangeMarker(currentTask.getEndOffset(), accumulatedTask.getEndOffset());
404         freeFormattingActions.add(new ReformatWithHeadingWhitespaceTask(rangeToProcess));
405         accumulatedTask = currentTask;
406       }
407       else if (!(accumulatedTask instanceof ReindentTask)) {
408         boolean withLeadingWhitespace = accumulatedTask instanceof ReformatWithHeadingWhitespaceTask;
409         if (accumulatedTask instanceof ReformatTask &&
410             currentTask instanceof ReformatWithHeadingWhitespaceTask &&
411             accumulatedTask.getStartOffset() == currentTask.getStartOffset()) {
412           withLeadingWhitespace = true;
413         }
414         else if (accumulatedTask instanceof ReformatWithHeadingWhitespaceTask &&
415             currentTask instanceof ReformatTask &&
416             accumulatedTask.getStartOffset() < currentTask.getStartOffset()) {
417           withLeadingWhitespace = false;
418         }
419         int newStart = Math.min(accumulatedTask.getStartOffset(), currentTask.getStartOffset());
420         int newEnd = Math.max(accumulatedTask.getEndOffset(), currentTask.getEndOffset());
421         RangeMarker rangeMarker;
422
423         if (accumulatedTask.getStartOffset() == newStart && accumulatedTask.getEndOffset() == newEnd) {
424           rangeMarker = accumulatedTask.getRange();
425         }
426         else if (currentTask.getStartOffset() == newStart && currentTask.getEndOffset() == newEnd) {
427           rangeMarker = currentTask.getRange();
428         }
429         else {
430           rangeMarker = document.createRangeMarker(newStart, newEnd);
431         }
432
433         if (withLeadingWhitespace) {
434           accumulatedTask = new ReformatWithHeadingWhitespaceTask(rangeMarker);
435         }
436         else {
437           accumulatedTask = new ReformatTask(rangeMarker);
438         }
439       }
440       // accumulatedTask is an instance of ReindentTask
441       else if (currentTask instanceof ReindentTask) {
442         // child indent is different from parent, the child condition
443         //         accumulatedTask.getStartOffset() <= currentTask.getStartOffset()
444         //      && accumulatedTask.getEndOffset() >= currentTask.getEndOffset()
445         // is always true here (ordered-by-end + up "if" in the method):
446
447         final CharSequence charsSequence = document.getCharsSequence();
448         int curEndOffset = currentTask.getEndOffset();
449         int curStartOffset = currentTask.getStartOffset();
450
451         // don't process ranges that have no new lines:
452         // optimization:  the case is covered by formatting task, or does not need the indent (fragment-in-the-middle-of-line).
453         // compatibility: inline blocks have wrong indent due to historical reasons (always for inline function calls).
454         if (charsSequence.subSequence(curStartOffset, curEndOffset).toString().indexOf('\n') != -1) {
455           if (accumulatedTask.getEndOffset() > curEndOffset) {
456             // tail of parent indent (the order is from-end-to-start)
457             // restore "canonical" indent for calibration of the indent
458             freeFormattingActions.add(new ReformatWithHeadingWhitespaceTask(document.createRangeMarker(curEndOffset, curEndOffset)));
459
460             // add the indent,
461             // push indent task directly, that is in correct from-end-to-start order.
462             indentActions.add(new ReindentTask(
463               document.createRangeMarker(curEndOffset, accumulatedTask.getEndOffset()),
464               ((ReindentTask)accumulatedTask).getOldIndent()));
465           }
466
467           if (accumulatedTask.getStartOffset() < curStartOffset) {
468             // head of parent indent (the order is from-end-to-start)
469             // the "canonical" indent for calibration of the indent should be prepared by previous tasks
470             // here don't care about.
471             // cannot push indent task directly, some child range task could be found.
472             rangesToProcess.add(new ReindentTask(
473               document.createRangeMarker(accumulatedTask.getStartOffset(), curStartOffset - 1),
474               ((ReindentTask)accumulatedTask).getOldIndent()));
475
476             //restore position
477             iterator = rangesToProcess.iterator();
478             //noinspection StatementWithEmptyBody
479             while (iterator.next().getRange() != currentTask.getRange()) ;
480           }
481
482           //body
483           final RangeMarker rangeToProcess = document.createRangeMarker(curStartOffset, curStartOffset);
484           freeFormattingActions.add(new ReformatWithHeadingWhitespaceTask(rangeToProcess));
485           accumulatedTask = currentTask;
486         }
487         //else do nothing, just drop unused ReindentTask [currentTask]
488       }
489       else {
490         continue;
491       }
492       iterator.remove();
493     }
494     if (accumulatedTask != null) {
495       if (accumulatedTask instanceof ReindentTask) {
496         indentActions.add((ReindentTask) accumulatedTask);
497       }
498       else {
499         freeFormattingActions.add(accumulatedTask);
500       }
501     }
502
503     final List<PostponedAction> result = new ArrayList<PostponedAction>();
504     Collections.reverse(freeFormattingActions);
505     Collections.reverse(indentActions);
506
507     if (!freeFormattingActions.isEmpty()) {
508       FormatTextRanges ranges = new FormatTextRanges();
509       for (PostprocessFormattingTask action : freeFormattingActions) {
510         TextRange range = TextRange.create(action);
511         ranges.add(range, action instanceof ReformatWithHeadingWhitespaceTask);
512       }
513       result.add(new ReformatRangesAction(ranges));
514     }
515
516     if (!indentActions.isEmpty()) {
517       ReindentRangesAction reindentRangesAction = new ReindentRangesAction();
518       for (ReindentTask action : indentActions) {
519         reindentRangesAction.add(action.getRange(), action.getOldIndent());
520       }
521       result.add(reindentRangesAction);
522     }
523
524     return result;
525   }
526
527   private static boolean canStickActionsTogether(final PostprocessFormattingTask currentTask,
528                                                  final PostprocessFormattingTask nextTask) {
529     // empty reformat markers can't be stuck together with any action
530     if (nextTask instanceof ReformatWithHeadingWhitespaceTask && nextTask.getStartOffset() == nextTask.getEndOffset()) return false;
531     if (currentTask instanceof ReformatWithHeadingWhitespaceTask && currentTask.getStartOffset() == currentTask.getEndOffset()) {
532       return false;
533     }
534     // reindent actions can't be be stuck at all
535     return !(currentTask instanceof ReindentTask);
536   }
537
538   private static void createActionsMap(final List<ASTNode> astNodes,
539                                        final FileViewProvider provider,
540                                        final TreeSet<PostprocessFormattingTask> rangesToProcess) {
541     final Set<ASTNode> nodesToProcess = new HashSet<ASTNode>(astNodes);
542     final Document document = provider.getDocument();
543     if (document == null) {
544       return;
545     }
546     for (final ASTNode node : astNodes) {
547       nodesToProcess.remove(node);
548       final FileElement fileElement = TreeUtil.getFileElement((TreeElement)node);
549       if (fileElement == null || ((PsiFile)fileElement.getPsi()).getViewProvider() != provider) continue;
550       final boolean isGenerated = CodeEditUtil.isNodeGenerated(node);
551
552       ((TreeElement)node).acceptTree(new RecursiveTreeElementVisitor() {
553         boolean inGeneratedContext = !isGenerated;
554
555         @Override
556         protected boolean visitNode(TreeElement element) {
557           if (nodesToProcess.contains(element)) return false;
558
559           final boolean currentNodeGenerated = CodeEditUtil.isNodeGenerated(element);
560           CodeEditUtil.setNodeGenerated(element, false);
561           if (currentNodeGenerated && !inGeneratedContext) {
562             rangesToProcess.add(new ReformatTask(document.createRangeMarker(element.getTextRange())));
563             inGeneratedContext = true;
564           }
565           if (!currentNodeGenerated && inGeneratedContext) {
566             if (element.getElementType() == TokenType.WHITE_SPACE) return false;
567             final int oldIndent = CodeEditUtil.getOldIndentation(element);
568             CodeEditUtil.setOldIndentation(element, -1);
569             LOG.assertTrue(oldIndent >= 0, "for not generated items old indentation must be defined: element " + element);
570             rangesToProcess.add(new ReindentTask(document.createRangeMarker(element.getTextRange()), oldIndent));
571             inGeneratedContext = false;
572           }
573           return true;
574         }
575
576         @Override
577         public void visitComposite(CompositeElement composite) {
578           boolean oldGeneratedContext = inGeneratedContext;
579           super.visitComposite(composite);
580           inGeneratedContext = oldGeneratedContext;
581         }
582
583         @Override
584         public void visitLeaf(LeafElement leaf) {
585           boolean oldGeneratedContext = inGeneratedContext;
586           super.visitLeaf(leaf);
587           inGeneratedContext = oldGeneratedContext;
588         }
589       });
590     }
591   }
592
593   private static void handleReformatMarkers(final FileViewProvider key, final TreeSet<PostprocessFormattingTask> rangesToProcess) {
594     final Document document = key.getDocument();
595     if (document == null) {
596       return;
597     }
598     for (final FileElement fileElement : ((SingleRootFileViewProvider)key).getKnownTreeRoots()) {
599       fileElement.acceptTree(new RecursiveTreeElementWalkingVisitor() {
600         @Override
601         protected void visitNode(TreeElement element) {
602           if (CodeEditUtil.isMarkedToReformatBefore(element)) {
603             CodeEditUtil.markToReformatBefore(element, false);
604             rangesToProcess.add(new ReformatWithHeadingWhitespaceTask(
605               document.createRangeMarker(element.getStartOffset(), element.getStartOffset()))
606             );
607           }
608           else if (CodeEditUtil.isMarkedToReformat(element)) {
609             CodeEditUtil.markToReformat(element, false);
610             rangesToProcess.add(new ReformatWithHeadingWhitespaceTask(
611               document.createRangeMarker(element.getStartOffset(), element.getStartOffset() + element.getTextLength()))
612             );
613           }
614           super.visitNode(element);
615         }
616       });
617     }
618   }
619
620   private static void adjustIndentationInRange(final PsiFile file,
621                                                final Document document,
622                                                final TextRange[] indents,
623                                                final int indentAdjustment) {
624     final CharSequence charsSequence = document.getCharsSequence();
625     for (final TextRange indent : indents) {
626       final String oldIndentStr = charsSequence.subSequence(indent.getStartOffset() + 1, indent.getEndOffset()).toString();
627       final int oldIndent = IndentHelperImpl.getIndent(file.getProject(), file.getFileType(), oldIndentStr, true);
628       final String newIndentStr = IndentHelperImpl
629         .fillIndent(file.getProject(), file.getFileType(), Math.max(oldIndent + indentAdjustment, 0));
630       document.replaceString(indent.getStartOffset() + 1, indent.getEndOffset(), newIndentStr);
631     }
632   }
633
634   @SuppressWarnings("StatementWithEmptyBody")
635   private static int getNewIndent(final PsiFile psiFile, final int firstWhitespace) {
636     final Document document = psiFile.getViewProvider().getDocument();
637     assert document != null;
638     final int startOffset = document.getLineStartOffset(document.getLineNumber(firstWhitespace));
639     int endOffset = startOffset;
640     final CharSequence charsSequence = document.getCharsSequence();
641     while (Character.isWhitespace(charsSequence.charAt(endOffset++))) ;
642     final String newIndentStr = charsSequence.subSequence(startOffset, endOffset - 1).toString();
643     return IndentHelperImpl.getIndent(psiFile.getProject(), psiFile.getFileType(), newIndentStr, true);
644   }
645
646   public boolean isDisabled() {
647     return myDisabledCounter > 0;
648   }
649
650   private CodeFormatterFacade getFormatterFacade(final FileViewProvider viewProvider) {
651     final CodeStyleSettings styleSettings = CodeStyleSettingsManager.getSettings(myPsiManager.getProject());
652     final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myPsiManager.getProject());
653     final Document document = viewProvider.getDocument();
654     assert document != null;
655     final CodeFormatterFacade codeFormatter = new CodeFormatterFacade(styleSettings);
656
657     documentManager.commitDocument(document);
658     return codeFormatter;
659   }
660
661   private abstract static class PostprocessFormattingTask implements Comparable<PostprocessFormattingTask>, Segment, Disposable {
662     @NotNull private final RangeMarker myRange;
663
664     public PostprocessFormattingTask(@NotNull RangeMarker rangeMarker) {
665       myRange = rangeMarker;
666     }
667
668     @Override
669     public int compareTo(@NotNull PostprocessFormattingTask o) {
670       RangeMarker o1 = myRange;
671       RangeMarker o2 = o.myRange;
672       if (o1.equals(o2)) return 0;
673       final int diff = o2.getEndOffset() - o1.getEndOffset();
674       if (diff == 0) {
675         if (o1.getStartOffset() == o2.getStartOffset()) return 0;
676         if (o1.getStartOffset() == o1.getEndOffset()) return -1; // empty ranges first
677         if (o2.getStartOffset() == o2.getEndOffset()) return 1; // empty ranges first
678         return o1.getStartOffset() - o2.getStartOffset();
679       }
680       return diff;
681     }
682
683     @NotNull
684     public RangeMarker getRange() {
685       return myRange;
686     }
687
688     @Override
689     public int getStartOffset() {
690       return myRange.getStartOffset();
691     }
692
693     @Override
694     public int getEndOffset() {
695       return myRange.getEndOffset();
696     }
697
698     @Override
699     public void dispose() {
700       if (myRange.isValid()) {
701         myRange.dispose();
702       }
703     }
704   }
705
706   private static class ReformatTask extends PostprocessFormattingTask {
707     public ReformatTask(RangeMarker rangeMarker) {
708       super(rangeMarker);
709     }
710   }
711
712   private static class ReformatWithHeadingWhitespaceTask extends PostprocessFormattingTask {
713     public ReformatWithHeadingWhitespaceTask(RangeMarker rangeMarker) {
714       super(rangeMarker);
715     }
716   }
717
718   private static class ReindentTask extends PostprocessFormattingTask {
719     private final int myOldIndent;
720
721     public ReindentTask(RangeMarker rangeMarker, int oldIndent) {
722       super(rangeMarker);
723       myOldIndent = oldIndent;
724     }
725
726     public int getOldIndent() {
727       return myOldIndent;
728     }
729   }
730
731   private interface PostponedAction extends Disposable {
732     void execute(FileViewProvider viewProvider);
733   }
734
735   private class ReformatRangesAction implements PostponedAction {
736     private final FormatTextRanges myRanges;
737
738     public ReformatRangesAction(FormatTextRanges ranges) {
739       myRanges = ranges;
740     }
741
742     @Override
743     public void execute(FileViewProvider viewProvider) {
744       final CodeFormatterFacade codeFormatter = getFormatterFacade(viewProvider);
745       codeFormatter.processText(viewProvider.getPsi(viewProvider.getBaseLanguage()), myRanges.ensureNonEmpty(), false);
746     }
747
748     @Override
749     public void dispose() {
750     }
751   }
752
753   private static class ReindentRangesAction implements PostponedAction {
754     private final List<Pair<Integer, RangeMarker>> myRangesToReindent = new ArrayList<Pair<Integer, RangeMarker>>();
755
756     public void add(RangeMarker rangeMarker, int oldIndent) {
757       myRangesToReindent.add(new Pair<Integer, RangeMarker>(oldIndent, rangeMarker));
758     }
759
760     @Override
761     public void execute(FileViewProvider viewProvider) {
762       final Document document = viewProvider.getDocument();
763       assert document != null;
764       final PsiFile psiFile = viewProvider.getPsi(viewProvider.getBaseLanguage());
765       for (Pair<Integer, RangeMarker> integerRangeMarkerPair : myRangesToReindent) {
766         RangeMarker marker = integerRangeMarkerPair.second;
767         final CharSequence charsSequence = document.getCharsSequence().subSequence(marker.getStartOffset(), marker.getEndOffset());
768         final int oldIndent = integerRangeMarkerPair.first;
769         final TextRange[] whitespaces = CharArrayUtil.getIndents(charsSequence, marker.getStartOffset());
770         final int indentAdjustment = getNewIndent(psiFile, marker.getStartOffset()) - oldIndent;
771         if (indentAdjustment != 0) adjustIndentationInRange(psiFile, document, whitespaces, indentAdjustment);
772       }
773     }
774
775     @Override
776     public void dispose() {
777       for (Pair<Integer, RangeMarker> pair : myRangesToReindent) {
778         RangeMarker marker = pair.second;
779         if (marker.isValid()) {
780           marker.dispose();
781         }
782       }
783     }
784   }
785
786   @TestOnly
787   public void clear() {
788     myReformatElements.clear();
789   }
790 }