Merge branch 'master' of git@git.labs.intellij.net:idea/community
[idea/community.git] / platform / lang-impl / src / com / intellij / psi / impl / source / PostprocessReformattingAspect.java
1 /*
2  * Copyright 2000-2009 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.psi.impl.source;
18
19 import com.intellij.lang.ASTNode;
20 import com.intellij.openapi.Disposable;
21 import com.intellij.openapi.application.ApplicationAdapter;
22 import com.intellij.openapi.application.ApplicationListener;
23 import com.intellij.openapi.application.ApplicationManager;
24 import com.intellij.openapi.command.CommandProcessor;
25 import com.intellij.openapi.diagnostic.Logger;
26 import com.intellij.openapi.editor.Document;
27 import com.intellij.openapi.editor.RangeMarker;
28 import com.intellij.openapi.fileTypes.FileType;
29 import com.intellij.openapi.fileTypes.FileTypeManager;
30 import com.intellij.openapi.progress.ProgressManager;
31 import com.intellij.openapi.project.Project;
32 import com.intellij.openapi.util.*;
33 import com.intellij.openapi.vfs.VirtualFile;
34 import com.intellij.pom.PomManager;
35 import com.intellij.pom.PomModelAspect;
36 import com.intellij.pom.event.PomModelEvent;
37 import com.intellij.pom.tree.TreeAspect;
38 import com.intellij.pom.tree.events.ChangeInfo;
39 import com.intellij.pom.tree.events.TreeChange;
40 import com.intellij.pom.tree.events.TreeChangeEvent;
41 import com.intellij.psi.*;
42 import com.intellij.psi.codeStyle.CodeStyleSettings;
43 import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
44 import com.intellij.psi.impl.PsiTreeDebugBuilder;
45 import com.intellij.psi.impl.source.codeStyle.CodeEditUtil;
46 import com.intellij.psi.impl.source.codeStyle.CodeFormatterFacade;
47 import com.intellij.psi.impl.source.codeStyle.Helper;
48 import com.intellij.psi.impl.source.codeStyle.HelperFactory;
49 import com.intellij.psi.impl.source.tree.*;
50 import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
51 import com.intellij.util.LocalTimeCounter;
52 import com.intellij.util.text.CharArrayUtil;
53 import org.jetbrains.annotations.NonNls;
54 import org.jetbrains.annotations.NotNull;
55
56 import java.util.*;
57
58 public class PostprocessReformattingAspect implements PomModelAspect, Disposable {
59   private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.PostprocessReformatingAspect");
60   private final Project myProject;
61   private final PsiManager myPsiManager;
62   private final TreeAspect myTreeAspect;
63   private final Map<FileViewProvider, List<ASTNode>> myReformatElements = new HashMap<FileViewProvider, List<ASTNode>>();
64   private volatile int myDisabledCounter = 0;
65   private final Set<FileViewProvider> myUpdatedProviders = new HashSet<FileViewProvider>();
66
67   private final ApplicationListener myApplicationListener = new ApplicationAdapter() {
68     public void writeActionStarted(final Object action) {
69       final CommandProcessor processor = CommandProcessor.getInstance();
70       if (processor != null) {
71         final Project project = processor.getCurrentCommandProject();
72         if (project == myProject) {
73           myPostponedCounter++;
74         }
75       }
76     }
77
78     public void writeActionFinished(final Object action) {
79       final CommandProcessor processor = CommandProcessor.getInstance();
80       if (processor != null) {
81         final Project project = processor.getCurrentCommandProject();
82         if (project == myProject) {
83           decrementPostponedCounter();
84         }
85       }
86     }
87   };
88
89   public PostprocessReformattingAspect(Project project, PsiManager psiManager, TreeAspect treeAspect) {
90     myProject = project;
91     myPsiManager = psiManager;
92     myTreeAspect = treeAspect;
93     PomManager.getModel(psiManager.getProject())
94       .registerAspect(PostprocessReformattingAspect.class, this, Collections.singleton((PomModelAspect)treeAspect));
95
96     ApplicationManager.getApplication().addApplicationListener(myApplicationListener);
97     Disposer.register(project, this);
98   }
99
100   public void dispose() {
101     ApplicationManager.getApplication().removeApplicationListener(myApplicationListener);
102   }
103
104   public void disablePostprocessFormattingInside(final Runnable runnable) {
105     disablePostprocessFormattingInside(new NullableComputable<Object>() {
106       public Object compute() {
107         runnable.run();
108         return null;
109       }
110     });
111   }
112
113   public <T> T disablePostprocessFormattingInside(Computable<T> computable) {
114     try {
115       myDisabledCounter++;
116       return computable.compute();
117     }
118     finally {
119       myDisabledCounter--;
120       LOG.assertTrue(myDisabledCounter > 0 || !isDisabled());
121     }
122   }
123
124   private int myPostponedCounter = 0;
125
126   public void postponeFormattingInside(final Runnable runnable) {
127     postponeFormattingInside(new NullableComputable<Object>() {
128       public Object compute() {
129         runnable.run();
130         return null;
131       }
132     });
133   }
134
135   public <T> T postponeFormattingInside(Computable<T> computable) {
136     try {
137       //if(myPostponedCounter == 0) myDisabled = false;
138       myPostponedCounter++;
139       return computable.compute();
140     }
141     finally {
142       decrementPostponedCounter();
143     }
144   }
145
146   private void decrementPostponedCounter() {
147     if (--myPostponedCounter == 0) {
148       if (!ApplicationManager.getApplication().isWriteAccessAllowed()) {
149         ApplicationManager.getApplication().runWriteAction(new Runnable() {
150           public void run() {
151             doPostponedFormatting();
152           }
153         });
154       }
155       else {
156         doPostponedFormatting();
157       }
158       //myDisabled = true;
159     }
160   }
161
162   private final Object LOCK = new Object();
163
164   private void atomic(Runnable r) {
165     synchronized (LOCK) {
166       ProgressManager.getInstance().executeNonCancelableSection(r);
167     }
168   }
169
170   public void update(final PomModelEvent event) {
171     atomic(new Runnable() {
172       public void run() {
173         if (isDisabled() || myPostponedCounter == 0 && !ApplicationManager.getApplication().isUnitTestMode()) return;
174         final TreeChangeEvent changeSet = (TreeChangeEvent)event.getChangeSet(myTreeAspect);
175         if (changeSet == null) return;
176         final PsiElement psiElement = changeSet.getRootElement().getPsi();
177         if (psiElement == null) return;
178         PsiFile containingFile = InjectedLanguageUtil.getTopLevelFile(psiElement);
179         final FileViewProvider viewProvider = containingFile.getViewProvider();
180
181         if (!viewProvider.isEventSystemEnabled()) return;
182         myUpdatedProviders.add(viewProvider);
183         for (final ASTNode node : changeSet.getChangedElements()) {
184           final TreeChange treeChange = changeSet.getChangesByElement(node);
185           for (final ASTNode affectedChild : treeChange.getAffectedChildren()) {
186             final ChangeInfo childChange = treeChange.getChangeByChild(affectedChild);
187             switch (childChange.getChangeType()) {
188               case ChangeInfo.ADD:
189               case ChangeInfo.REPLACE:
190                 postponeFormatting(viewProvider, affectedChild);
191                 break;
192               case ChangeInfo.CONTENTS_CHANGED:
193                 if (!CodeEditUtil.isNodeGenerated(affectedChild)) {
194                   ((TreeElement)affectedChild).acceptTree(new RecursiveTreeElementWalkingVisitor() {
195                     protected void visitNode(TreeElement element) {
196                       if (CodeEditUtil.isNodeGenerated(element)) {
197                         postponeFormatting(viewProvider, element);
198                         return;
199                       }
200                       super.visitNode(element);
201                     }
202                   });
203                 }
204                 break;
205             }
206           }
207         }
208       }
209     });
210   }
211
212   public void doPostponedFormatting() {
213     atomic(new Runnable() {
214       public void run() {
215         if (isDisabled()) return;
216         try {
217           for (final FileViewProvider viewProvider : myUpdatedProviders) {
218             doPostponedFormatting(viewProvider);
219           }
220         }
221         finally {
222           LOG.assertTrue(myReformatElements.isEmpty());
223           myUpdatedProviders.clear();
224           myReformatElements.clear();
225         }
226       }
227     });
228   }
229
230   public void doPostponedFormatting(final FileViewProvider viewProvider) {
231     atomic(new Runnable() {
232       public void run() {
233         if (isDisabled()) return;
234
235         disablePostprocessFormattingInside(new Runnable() {
236           public void run() {
237             doPostponedFormattingInner(viewProvider);
238           }
239         });
240       }
241     });
242   }
243
244   public boolean isViewProviderLocked(final FileViewProvider fileViewProvider) {
245     return myReformatElements.containsKey(fileViewProvider);
246   }
247
248   public static PostprocessReformattingAspect getInstance(Project project) {
249     return project.getComponent(PostprocessReformattingAspect.class);
250   }
251
252   private void postponeFormatting(final FileViewProvider viewProvider, final ASTNode child) {
253     if (!CodeEditUtil.isNodeGenerated(child) && child.getElementType() != TokenType.WHITE_SPACE) {
254       final int oldIndent = CodeEditUtil.getOldIndentation(child);
255       LOG.assertTrue(oldIndent >= 0,
256                      "for not generated items old indentation must be defined: element=" + child + ", text=" + child.getText());
257     }
258     List<ASTNode> list = myReformatElements.get(viewProvider);
259     if (list == null) {
260       list = new ArrayList<ASTNode>();
261       myReformatElements.put(viewProvider, list);
262     }
263     list.add(child);
264   }
265
266   private void doPostponedFormattingInner(final FileViewProvider key) {
267
268
269     final List<ASTNode> astNodes = myReformatElements.remove(key);
270     final Document document = key.getDocument();
271     // Sort ranges by end offsets so that we won't need any offset adjustment after reformat or reindent
272     if (document == null /*|| documentManager.isUncommited(document) TODO */) return;
273
274     final VirtualFile virtualFile = key.getVirtualFile();
275     if (!virtualFile.isValid()) return;
276
277     final TreeMap<RangeMarker, PostponedAction> rangesToProcess = new TreeMap<RangeMarker, PostponedAction>(new Comparator<RangeMarker>() {
278       public int compare(final RangeMarker o1, final RangeMarker o2) {
279         if (o1.equals(o2)) return 0;
280         final int diff = o2.getEndOffset() - o1.getEndOffset();
281         if (diff == 0) {
282           if (o1.getStartOffset() == o2.getStartOffset()) return 0;
283           if (o1.getStartOffset() == o1.getEndOffset()) return -1; // empty ranges first
284           if (o2.getStartOffset() == o2.getEndOffset()) return 1; // empty ranges first
285           return o1.getStartOffset() - o2.getStartOffset();
286         }
287         return diff;
288       }
289     });
290
291     // process all roots in viewProvider to find marked for reformat before elements and create appropriate ragge markers
292     handleReformatMarkers(key, rangesToProcess);
293
294     // then we create ranges by changed nodes. One per node. There ranges can instersect. Ranges are sorted by end offset.
295     if (astNodes != null) createActionsMap(astNodes, key, rangesToProcess);
296
297     if ("true".equals(System.getProperty("check.psi.is.valid")) && ApplicationManager.getApplication().isUnitTestMode()) {
298       checkPsiIsCorrect(key);
299     }
300
301     while (!rangesToProcess.isEmpty()) {
302       // now we have to normalize actions so that they not intersect and ordered in most appropriate way
303       // (free reformating -> reindent -> formating under reindent)
304       final List<Pair<RangeMarker, ? extends PostponedAction>> normalizedActions =
305         normalizeAndReorderPostponedActions(rangesToProcess, document);
306
307       // only in following loop real changes in document are made
308       for (final Pair<RangeMarker, ? extends PostponedAction> normalizedAction : normalizedActions) {
309         CodeStyleSettings settings = CodeStyleSettingsManager.getSettings(myPsiManager.getProject());
310         boolean old = settings.ENABLE_JAVADOC_FORMATTING;
311         settings.ENABLE_JAVADOC_FORMATTING = false;
312         try {
313           normalizedAction.getSecond().processRange(normalizedAction.getFirst(), key);
314         }
315         finally {
316           settings.ENABLE_JAVADOC_FORMATTING = old;
317         }
318       }
319     }
320   }
321
322   private void checkPsiIsCorrect(final FileViewProvider key) {
323     PsiFile actualPsi = key.getPsi(key.getBaseLanguage());
324
325     PsiTreeDebugBuilder treeDebugBuilder = new PsiTreeDebugBuilder().setShowErrorElements(false).setShowWhiteSpaces(false);
326
327     String actualPsiTree = treeDebugBuilder.psiToString(actualPsi);
328
329     String fileName = key.getVirtualFile().getName();
330     PsiFile psi = PsiFileFactory.getInstance(myProject)
331       .createFileFromText(fileName, FileTypeManager.getInstance().getFileTypeByFileName(fileName), actualPsi.getNode().getText(),
332                           LocalTimeCounter.currentTime(), false);
333
334     if (actualPsi.getClass().equals(psi.getClass())) {
335       String expectedPsi = treeDebugBuilder.psiToString(psi);
336
337       if (!expectedPsi.equals(actualPsiTree)) {
338         myReformatElements.clear();
339         assert expectedPsi.equals(actualPsiTree) : "Refactored psi should be the same as result of parsing";
340       }
341     }
342
343
344   }
345
346   private List<Pair<RangeMarker, ? extends PostponedAction>> normalizeAndReorderPostponedActions(final TreeMap<RangeMarker, PostponedAction> rangesToProcess,
347                                                                                                  Document document) {
348     final List<Pair<RangeMarker, ReformatAction>> freeFormatingActions = new ArrayList<Pair<RangeMarker, ReformatAction>>();
349     final List<Pair<RangeMarker, ReindentAction>> indentActions = new ArrayList<Pair<RangeMarker, ReindentAction>>();
350
351     RangeMarker accumulatedRange = null;
352     PostponedAction accumulatedRangeAction = null;
353     Iterator<Map.Entry<RangeMarker, PostponedAction>> iterator = rangesToProcess.entrySet().iterator();
354     while (iterator.hasNext()) {
355       final Map.Entry<RangeMarker, PostponedAction> entry = iterator.next();
356       final RangeMarker textRange = entry.getKey();
357       final PostponedAction action = entry.getValue();
358       if (accumulatedRange == null) {
359         accumulatedRange = textRange;
360         accumulatedRangeAction = action;
361         iterator.remove();
362       }
363       else if (accumulatedRange.getStartOffset() > textRange.getEndOffset() ||
364                (accumulatedRange.getStartOffset() == textRange.getEndOffset() &&
365                 !canStickActionsTogether(accumulatedRangeAction, accumulatedRange, action, textRange))) {
366         // action can be pushed
367         if (accumulatedRangeAction instanceof ReindentAction) {
368           indentActions.add(new Pair<RangeMarker, ReindentAction>(accumulatedRange, (ReindentAction)accumulatedRangeAction));
369         }
370         else {
371           freeFormatingActions.add(new Pair<RangeMarker, ReformatAction>(accumulatedRange, (ReformatAction)accumulatedRangeAction));
372         }
373
374         accumulatedRange = textRange;
375         accumulatedRangeAction = action;
376         iterator.remove();
377       }
378       else if (accumulatedRangeAction instanceof ReformatAction && action instanceof ReindentAction) {
379         // split accumulated reformat range into two
380         if (accumulatedRange.getStartOffset() < textRange.getStartOffset()) {
381           final RangeMarker endOfRange = document.createRangeMarker(accumulatedRange.getStartOffset(), textRange.getStartOffset());
382           // add heading reformat part
383           rangesToProcess.put(endOfRange, accumulatedRangeAction);
384           // and manage heading whitespace because formatter does not edit it in previous action
385           iterator = rangesToProcess.entrySet().iterator();
386           //noinspection StatementWithEmptyBody
387           while (iterator.next().getKey() != textRange) ;
388         }
389         final RangeMarker rangeToProcess = document.createRangeMarker(textRange.getEndOffset(), accumulatedRange.getEndOffset());
390         freeFormatingActions.add(new Pair<RangeMarker, ReformatAction>(rangeToProcess, new ReformatWithHeadingWhitespaceAction()));
391         accumulatedRange = textRange;
392         accumulatedRangeAction = action;
393         iterator.remove();
394       }
395       else {
396         if (!(accumulatedRangeAction instanceof ReindentAction)) {
397           iterator.remove();
398           if (accumulatedRangeAction instanceof ReformatAction &&
399               action instanceof ReformatWithHeadingWhitespaceAction &&
400               accumulatedRange.getStartOffset() == textRange.getStartOffset() ||
401               accumulatedRangeAction instanceof ReformatWithHeadingWhitespaceAction &&
402               action instanceof ReformatAction &&
403               accumulatedRange.getStartOffset() < textRange.getStartOffset()) {
404             accumulatedRangeAction = action;
405           }
406           accumulatedRange = document.createRangeMarker(Math.min(accumulatedRange.getStartOffset(), textRange.getStartOffset()),
407                                                         Math.max(accumulatedRange.getEndOffset(), textRange.getEndOffset()));
408         }
409         else if (action instanceof ReindentAction) {
410           iterator.remove();
411         } // TODO[ik]: need to be fixed to correctly process indent inside indent
412       }
413     }
414     if (accumulatedRange != null) {
415       if (accumulatedRangeAction instanceof ReindentAction) {
416         indentActions.add(new Pair<RangeMarker, ReindentAction>(accumulatedRange, (ReindentAction)accumulatedRangeAction));
417       }
418       else {
419         freeFormatingActions.add(new Pair<RangeMarker, ReformatAction>(accumulatedRange, (ReformatAction)accumulatedRangeAction));
420       }
421     }
422
423     final List<Pair<RangeMarker, ? extends PostponedAction>> result =
424       new ArrayList<Pair<RangeMarker, ? extends PostponedAction>>(rangesToProcess.size());
425     Collections.reverse(freeFormatingActions);
426     Collections.reverse(indentActions);
427     result.addAll(freeFormatingActions);
428     result.addAll(indentActions);
429     return result;
430   }
431
432   private boolean canStickActionsTogether(final PostponedAction currentAction,
433                                           final RangeMarker currentRange,
434                                           final PostponedAction nextAction,
435                                           final RangeMarker nextRange) {
436     // empty reformat markers can't sticked together with any action
437     if (nextAction instanceof ReformatWithHeadingWhitespaceAction && nextRange.getStartOffset() == nextRange.getEndOffset()) return false;
438     if (currentAction instanceof ReformatWithHeadingWhitespaceAction && currentRange.getStartOffset() == currentRange.getEndOffset()) {
439       return false;
440     }
441     // reindent actions can't be sticked at all
442     return !(currentAction instanceof ReindentAction);
443   }
444
445   private void createActionsMap(final List<ASTNode> astNodes,
446                                 final FileViewProvider provider,
447                                 final TreeMap<RangeMarker, PostponedAction> rangesToProcess) {
448     final Set<ASTNode> nodesToProcess = new HashSet<ASTNode>(astNodes);
449     final Document document = provider.getDocument();
450     for (final ASTNode node : astNodes) {
451       nodesToProcess.remove(node);
452       final FileElement fileElement = TreeUtil.getFileElement((TreeElement)node);
453       if (fileElement == null || ((PsiFile)fileElement.getPsi()).getViewProvider() != provider) continue;
454       final boolean isGenerated = CodeEditUtil.isNodeGenerated(node);
455
456       ((TreeElement)node).acceptTree(new RecursiveTreeElementVisitor() {
457         boolean inGeneratedContext = !isGenerated;
458
459         protected boolean visitNode(TreeElement element) {
460           if (nodesToProcess.contains(element)) return false;
461           final boolean currentNodeGenerated = CodeEditUtil.isNodeGenerated(element);
462           CodeEditUtil.setNodeGenerated(element, false);
463           if (currentNodeGenerated && !inGeneratedContext) {
464             rangesToProcess.put(document.createRangeMarker(element.getTextRange()), new ReformatAction());
465             inGeneratedContext = true;
466           }
467           if (!currentNodeGenerated && inGeneratedContext) {
468             if (element.getElementType() == TokenType.WHITE_SPACE) return false;
469             final int oldIndent = CodeEditUtil.getOldIndentation(element);
470             LOG.assertTrue(oldIndent >= 0, "for not generated items old indentation must be defined");
471             rangesToProcess.put(document.createRangeMarker(element.getTextRange()), new ReindentAction(oldIndent));
472             inGeneratedContext = false;
473           }
474           return true;
475         }
476
477         @Override
478         public void visitComposite(CompositeElement composite) {
479           boolean oldGeneratedContext = inGeneratedContext;
480           super.visitComposite(composite);
481           inGeneratedContext = oldGeneratedContext;
482         }
483
484         @Override
485         public void visitLeaf(LeafElement leaf) {
486           boolean oldGeneratedContext = inGeneratedContext;
487           super.visitLeaf(leaf);
488           inGeneratedContext = oldGeneratedContext;
489         }
490       });
491     }
492   }
493
494   private void handleReformatMarkers(final FileViewProvider key, final TreeMap<RangeMarker, PostponedAction> rangesToProcess) {
495     final Document document = key.getDocument();
496     for (final FileElement fileElement : ((SingleRootFileViewProvider)key).getKnownTreeRoots()) {
497       fileElement.acceptTree(new RecursiveTreeElementWalkingVisitor() {
498         protected void visitNode(TreeElement element) {
499           if (CodeEditUtil.isMarkedToReformatBefore(element)) {
500             CodeEditUtil.markToReformatBefore(element, false);
501             rangesToProcess.put(document.createRangeMarker(element.getStartOffset(), element.getStartOffset()),
502                                 new ReformatWithHeadingWhitespaceAction());
503           }
504           super.visitNode(element);
505         }
506       });
507     }
508   }
509
510   private static void adjustIndentationInRange(final PsiFile file,
511                                                final Document document,
512                                                final TextRange[] indents,
513                                                final int indentAdjustment) {
514     final Helper formatHelper = HelperFactory.createHelper(file.getFileType(), file.getProject());
515     final CharSequence charsSequence = document.getCharsSequence();
516     for (final TextRange indent : indents) {
517       final String oldIndentStr = charsSequence.subSequence(indent.getStartOffset() + 1, indent.getEndOffset()).toString();
518       final int oldIndent = formatHelper.getIndent(oldIndentStr, true);
519       final String newIndentStr = formatHelper.fillIndent(Math.max(oldIndent + indentAdjustment, 0));
520       document.replaceString(indent.getStartOffset() + 1, indent.getEndOffset(), newIndentStr);
521     }
522   }
523
524   private static int getNewIndent(final PsiFile psiFile, final int firstWhitespace) {
525     final Helper formatHelper = HelperFactory.createHelper(psiFile.getFileType(), psiFile.getProject());
526     final Document document = psiFile.getViewProvider().getDocument();
527     final int startOffset = document.getLineStartOffset(document.getLineNumber(firstWhitespace));
528     int endOffset = startOffset;
529     final CharSequence charsSequence = document.getCharsSequence();
530     while (Character.isWhitespace(charsSequence.charAt(endOffset++))) ;
531     final String newIndentStr = charsSequence.subSequence(startOffset, endOffset - 1).toString();
532     return formatHelper.getIndent(newIndentStr, true);
533   }
534
535   public boolean isDisabled() {
536     return myDisabledCounter > 0;
537   }
538
539   private CodeFormatterFacade getFormatterFacade(final FileViewProvider viewProvider) {
540     final CodeStyleSettings styleSettings = CodeStyleSettingsManager.getSettings(myPsiManager.getProject());
541     final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myPsiManager.getProject());
542     final Document document = viewProvider.getDocument();
543     final FileType fileType = viewProvider.getVirtualFile().getFileType();
544     final Helper helper = HelperFactory.createHelper(fileType, myPsiManager.getProject());
545     final CodeFormatterFacade codeFormatter = new CodeFormatterFacade(styleSettings, helper);
546
547     documentManager.commitDocument(document);
548     return codeFormatter;
549   }
550
551   private interface PostponedAction {
552     void processRange(RangeMarker marker, final FileViewProvider viewProvider);
553   }
554
555   private class ReformatAction implements PostponedAction {
556     public void processRange(RangeMarker marker, final FileViewProvider viewProvider) {
557       final CodeFormatterFacade codeFormatter = getFormatterFacade(viewProvider);
558       codeFormatter.processTextWithoutHeadWhitespace(viewProvider.getPsi(viewProvider.getBaseLanguage()), marker.getStartOffset(),
559                                                      marker.getEndOffset());
560     }
561   }
562
563   private class ReformatWithHeadingWhitespaceAction extends ReformatAction {
564     public void processRange(RangeMarker marker, final FileViewProvider viewProvider) {
565       final CodeFormatterFacade codeFormatter = getFormatterFacade(viewProvider);
566       codeFormatter.processText(viewProvider.getPsi(viewProvider.getBaseLanguage()), marker.getStartOffset(),
567                                 marker.getStartOffset() == marker.getEndOffset() ? marker.getEndOffset() + 1 : marker.getEndOffset());
568     }
569
570   }
571
572
573   private static class ReindentAction implements PostponedAction {
574     private final int myOldIndent;
575
576     public ReindentAction(final int oldIndent) {
577       myOldIndent = oldIndent;
578     }
579
580     public void processRange(RangeMarker marker, final FileViewProvider viewProvider) {
581       final Document document = viewProvider.getDocument();
582       final PsiFile psiFile = viewProvider.getPsi(viewProvider.getBaseLanguage());
583       final CharSequence charsSequence = document.getCharsSequence().subSequence(marker.getStartOffset(), marker.getEndOffset());
584       final int oldIndent = getOldIndent();
585       final TextRange[] whitespaces = CharArrayUtil.getIndents(charsSequence, marker.getStartOffset());
586       final int indentAdjustment = getNewIndent(psiFile, marker.getStartOffset()) - oldIndent;
587       if (indentAdjustment != 0) adjustIndentationInRange(psiFile, document, whitespaces, indentAdjustment);
588     }
589
590     private int getOldIndent() {
591       return myOldIndent;
592     }
593
594   }
595
596   public void projectOpened() {
597   }
598
599   public void projectClosed() {
600   }
601
602   @NotNull
603   @NonNls
604   public String getComponentName() {
605     return "Postponed reformatting model";
606   }
607
608   public void initComponent() {
609   }
610
611   public void disposeComponent() {
612   }
613 }