do not use up stack space, traverse threaded tree consecutively
[fedora-idea.git] / lang-impl / src / com / intellij / psi / impl / source / PostprocessReformattingAspect.java
blobf3f15ead1d295d5f219dc9a56f6cb28dbb9c8e0b
1 package com.intellij.psi.impl.source;
3 import com.intellij.lang.ASTNode;
4 import com.intellij.openapi.Disposable;
5 import com.intellij.openapi.application.ApplicationAdapter;
6 import com.intellij.openapi.application.ApplicationListener;
7 import com.intellij.openapi.application.ApplicationManager;
8 import com.intellij.openapi.command.CommandProcessor;
9 import com.intellij.openapi.diagnostic.Logger;
10 import com.intellij.openapi.editor.Document;
11 import com.intellij.openapi.editor.RangeMarker;
12 import com.intellij.openapi.fileTypes.FileType;
13 import com.intellij.openapi.fileTypes.FileTypeManager;
14 import com.intellij.openapi.project.Project;
15 import com.intellij.openapi.util.Computable;
16 import com.intellij.openapi.util.Disposer;
17 import com.intellij.openapi.util.Pair;
18 import com.intellij.openapi.util.TextRange;
19 import com.intellij.pom.PomManager;
20 import com.intellij.pom.PomModelAspect;
21 import com.intellij.pom.event.PomModelEvent;
22 import com.intellij.pom.tree.TreeAspect;
23 import com.intellij.pom.tree.events.ChangeInfo;
24 import com.intellij.pom.tree.events.TreeChange;
25 import com.intellij.pom.tree.events.TreeChangeEvent;
26 import com.intellij.psi.*;
27 import com.intellij.psi.codeStyle.CodeStyleSettings;
28 import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
29 import com.intellij.psi.impl.PsiTreeDebugBuilder;
30 import com.intellij.psi.impl.source.codeStyle.CodeEditUtil;
31 import com.intellij.psi.impl.source.codeStyle.CodeFormatterFacade;
32 import com.intellij.psi.impl.source.codeStyle.Helper;
33 import com.intellij.psi.impl.source.codeStyle.HelperFactory;
34 import com.intellij.psi.impl.source.tree.*;
35 import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
36 import com.intellij.util.LocalTimeCounter;
37 import com.intellij.util.text.CharArrayUtil;
38 import junit.framework.Assert;
39 import org.jetbrains.annotations.NonNls;
40 import org.jetbrains.annotations.NotNull;
42 import java.util.*;
44 public class PostprocessReformattingAspect implements PomModelAspect, Disposable {
45 private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.PostprocessReformatingAspect");
46 private final Project myProject;
47 private final PsiManager myPsiManager;
48 private final TreeAspect myTreeAspect;
49 private final Map<FileViewProvider, List<ASTNode>> myReformatElements = new HashMap<FileViewProvider, List<ASTNode>>();
50 private volatile int myDisabledCounter = 0;
51 private final Set<FileViewProvider> myUpdatedProviders = new HashSet<FileViewProvider>();
53 private final ApplicationListener myApplicationListener = new ApplicationAdapter() {
54 public void writeActionStarted(final Object action) {
55 final CommandProcessor processor = CommandProcessor.getInstance();
56 if (processor != null) {
57 final Project project = processor.getCurrentCommandProject();
58 if(project == myProject) {
59 myPostponedCounter++;
64 public void writeActionFinished(final Object action) {
65 final CommandProcessor processor = CommandProcessor.getInstance();
66 if (processor != null) {
67 final Project project = processor.getCurrentCommandProject();
68 if(project == myProject) {
69 decrementPostponedCounter();
75 public PostprocessReformattingAspect(Project project, PsiManager psiManager, TreeAspect treeAspect) {
76 myProject = project;
77 myPsiManager = psiManager;
78 myTreeAspect = treeAspect;
79 PomManager.getModel(psiManager.getProject()).registerAspect(PostprocessReformattingAspect.class, this, Collections.singleton((PomModelAspect)treeAspect));
81 ApplicationManager.getApplication().addApplicationListener(myApplicationListener);
82 Disposer.register(project, this);
85 public void dispose() {
86 ApplicationManager.getApplication().removeApplicationListener(myApplicationListener);
89 public void disablePostprocessFormattingInside(final Runnable runnable) {
90 disablePostprocessFormattingInside(new Computable<Object>() {
91 public Object compute() {
92 runnable.run();
93 return null;
95 });
98 public <T> T disablePostprocessFormattingInside(Computable<T> computable){
99 try {
100 myDisabledCounter++;
101 return computable.compute();
103 finally {
104 myDisabledCounter--;
105 LOG.assertTrue(myDisabledCounter > 0 || !isDisabled());
109 private int myPostponedCounter = 0;
110 public void postponeFormattingInside(final Runnable runnable) {
111 postponeFormattingInside(new Computable<Object>() {
112 public Object compute() {
113 runnable.run();
114 return null;
119 public <T> T postponeFormattingInside(Computable<T> computable){
120 try {
121 //if(myPostponedCounter == 0) myDisabled = false;
122 myPostponedCounter++;
123 return computable.compute();
125 finally {
126 decrementPostponedCounter();
130 private void decrementPostponedCounter() {
131 if (--myPostponedCounter == 0) {
132 if(!ApplicationManager.getApplication().isWriteAccessAllowed()){
133 ApplicationManager.getApplication().runWriteAction(new Runnable() {
134 public void run() {
135 doPostponedFormatting();
139 else doPostponedFormatting();
140 //myDisabled = true;
144 public void update(PomModelEvent event) {
145 synchronized(PsiLock.LOCK){
146 if(isDisabled() || myPostponedCounter == 0 && !ApplicationManager.getApplication().isUnitTestMode()) return;
147 final TreeChangeEvent changeSet = (TreeChangeEvent)event.getChangeSet(myTreeAspect);
148 if(changeSet == null) return;
149 final PsiElement psiElement = changeSet.getRootElement().getPsi();
150 if(psiElement == null) return;
151 PsiFile containingFile = InjectedLanguageUtil.getTopLevelFile(psiElement);
152 final FileViewProvider viewProvider = containingFile.getViewProvider();
154 if(!viewProvider.isEventSystemEnabled()) return;
155 myUpdatedProviders.add(viewProvider);
156 for (final ASTNode node : changeSet.getChangedElements()) {
157 final TreeChange treeChange = changeSet.getChangesByElement(node);
158 for (final ASTNode affectedChild : treeChange.getAffectedChildren()) {
159 final ChangeInfo childChange = treeChange.getChangeByChild(affectedChild);
160 switch(childChange.getChangeType()){
161 case ChangeInfo.ADD:
162 case ChangeInfo.REPLACE:
163 postponeFormatting(viewProvider, affectedChild);
164 break;
165 case ChangeInfo.CONTENTS_CHANGED:
166 if(!CodeEditUtil.isNodeGenerated(affectedChild))
167 ((TreeElement)affectedChild).acceptTree(new RecursiveTreeElementWalkingVisitor(){
168 protected boolean visitNode(TreeElement element) {
169 if(CodeEditUtil.isNodeGenerated(element)){
170 postponeFormatting(viewProvider, element);
171 return false;
173 return true;
176 break;
183 public void doPostponedFormatting(){
184 synchronized(PsiLock.LOCK){
185 if(isDisabled()) return;
186 try{
187 for (final FileViewProvider viewProvider : myUpdatedProviders) {
188 doPostponedFormatting(viewProvider);
191 finally {
192 LOG.assertTrue(myReformatElements.isEmpty());
193 myUpdatedProviders.clear();
194 myReformatElements.clear();
199 public void doPostponedFormatting(final FileViewProvider viewProvider) {
200 synchronized(PsiLock.LOCK){
201 if(isDisabled()) return;
203 disablePostprocessFormattingInside(new Runnable() {
204 public void run() {
205 doPostponedFormattingInner(viewProvider);
211 public boolean isViewProviderLocked(final FileViewProvider fileViewProvider) {
212 return myReformatElements.containsKey(fileViewProvider);
215 public static PostprocessReformattingAspect getInstance(Project project) {
216 return project.getComponent(PostprocessReformattingAspect.class);
219 private void postponeFormatting(final FileViewProvider viewProvider, final ASTNode child) {
220 if (!CodeEditUtil.isNodeGenerated(child) && child.getElementType() != TokenType.WHITE_SPACE) {
221 final int oldIndent = CodeEditUtil.getOldIndentation(child);
222 LOG.assertTrue(oldIndent >= 0, "for not generated items old indentation must be defined: element=" + child + ", text=" + child.getText());
224 List<ASTNode> list = myReformatElements.get(viewProvider);
225 if (list == null) {
226 list = new ArrayList<ASTNode>();
227 myReformatElements.put(viewProvider, list);
229 list.add(child);
232 private void doPostponedFormattingInner(final FileViewProvider key) {
235 final List<ASTNode> astNodes = myReformatElements.remove(key);
236 final Document document = key.getDocument();
237 // Sort ranges by end offsets so that we won't need any offset adjustment after reformat or reindent
238 if (document == null /*|| documentManager.isUncommited(document) TODO */) return;
240 final TreeMap<RangeMarker, PostponedAction> rangesToProcess = new TreeMap<RangeMarker, PostponedAction>(new Comparator<RangeMarker>() {
241 public int compare(final RangeMarker o1, final RangeMarker o2) {
242 if (o1.equals(o2)) return 0;
243 final int diff = o2.getEndOffset() - o1.getEndOffset();
244 if (diff == 0){
245 if(o1.getStartOffset() == o2.getStartOffset()) return 0;
246 if(o1.getStartOffset() == o1.getEndOffset()) return -1; // empty ranges first
247 if(o2.getStartOffset() == o2.getEndOffset()) return 1; // empty ranges first
248 return o1.getStartOffset() - o2.getStartOffset();
250 return diff;
254 // process all roots in viewProvider to find marked for reformat before elements and create appropriate ragge markers
255 handleReformatMarkers(key, rangesToProcess);
257 // then we create ranges by changed nodes. One per node. There ranges can instersect. Ranges are sorted by end offset.
258 if (astNodes != null) createActionsMap(astNodes, key, rangesToProcess);
260 if ("true".equals(System.getProperty("check.psi.is.valid")) && ApplicationManager.getApplication().isUnitTestMode()) {
261 checkPsiIsCorrect(key);
264 while(!rangesToProcess.isEmpty()){
265 // now we have to normalize actions so that they not intersect and ordered in most appropriate way
266 // (free reformating -> reindent -> formating under reindent)
267 final List<Pair<RangeMarker, ? extends PostponedAction>> normalizedActions = normalizeAndReorderPostponedActions(rangesToProcess, document);
269 // only in following loop real changes in document are made
270 for (final Pair<RangeMarker, ? extends PostponedAction> normalizedAction : normalizedActions) {
271 CodeStyleSettings settings = CodeStyleSettingsManager.getSettings(myPsiManager.getProject());
272 boolean old = settings.ENABLE_JAVADOC_FORMATTING;
273 settings.ENABLE_JAVADOC_FORMATTING = false;
274 try {
275 normalizedAction.getSecond().processRange(normalizedAction.getFirst(), key);
277 finally {
278 settings.ENABLE_JAVADOC_FORMATTING = old;
284 private void checkPsiIsCorrect(final FileViewProvider key) {
285 PsiFile actualPsi = key.getPsi(key.getBaseLanguage());
287 PsiTreeDebugBuilder treeDebugBuilder = new PsiTreeDebugBuilder().setShowErrorElements(false).setShowWhiteSpaces(false);
289 String actualPsiTree = treeDebugBuilder.psiToString(actualPsi);
291 String fileName = key.getVirtualFile().getName();
292 PsiFile psi = PsiFileFactory.getInstance(myProject)
293 .createFileFromText(fileName, FileTypeManager.getInstance().getFileTypeByFileName(fileName), actualPsi.getNode().getText(),
294 LocalTimeCounter.currentTime(), false);
296 if (actualPsi.getClass().equals(psi.getClass())) {
297 String expectedPsi = treeDebugBuilder.psiToString(psi);
299 if (!expectedPsi.equals(actualPsiTree)) {
300 myReformatElements.clear();
301 Assert.assertEquals("Refactored psi should be the same as result of parsing", expectedPsi, actualPsiTree);
308 private List<Pair<RangeMarker, ? extends PostponedAction>> normalizeAndReorderPostponedActions(final TreeMap<RangeMarker, PostponedAction> rangesToProcess, Document document) {
309 final List<Pair<RangeMarker, ReformatAction>> freeFormatingActions = new ArrayList<Pair<RangeMarker, ReformatAction>>();
310 final List<Pair<RangeMarker, ReindentAction>> indentActions = new ArrayList<Pair<RangeMarker, ReindentAction>>();
312 RangeMarker accumulatedRange = null;
313 PostponedAction accumulatedRangeAction = null;
314 Iterator<Map.Entry<RangeMarker, PostponedAction>> iterator = rangesToProcess.entrySet().iterator();
315 while (iterator.hasNext()) {
316 final Map.Entry<RangeMarker, PostponedAction> entry = iterator.next();
317 final RangeMarker textRange = entry.getKey();
318 final PostponedAction action = entry.getValue();
319 if (accumulatedRange == null) {
320 accumulatedRange = textRange;
321 accumulatedRangeAction = action;
322 iterator.remove();
324 else if (accumulatedRange.getStartOffset() > textRange.getEndOffset() ||
325 (accumulatedRange.getStartOffset() == textRange.getEndOffset() &&
326 !canStickActionsTogether(accumulatedRangeAction, accumulatedRange, action, textRange))) {
327 // action can be pushed
328 if (accumulatedRangeAction instanceof ReindentAction)
329 indentActions.add(new Pair<RangeMarker, ReindentAction>(accumulatedRange, (ReindentAction)accumulatedRangeAction));
330 else
331 freeFormatingActions.add(new Pair<RangeMarker, ReformatAction>(accumulatedRange, (ReformatAction)accumulatedRangeAction));
333 accumulatedRange = textRange;
334 accumulatedRangeAction = action;
335 iterator.remove();
337 else if (accumulatedRangeAction instanceof ReformatAction && action instanceof ReindentAction) {
338 // split accumulated reformat range into two
339 if (accumulatedRange.getStartOffset() < textRange.getStartOffset()) {
340 final RangeMarker endOfRange = document.createRangeMarker(accumulatedRange.getStartOffset(), textRange.getStartOffset());
341 // add heading reformat part
342 rangesToProcess.put(endOfRange, accumulatedRangeAction);
343 // and manage heading whitespace because formatter does not edit it in previous action
344 iterator = rangesToProcess.entrySet().iterator();
345 //noinspection StatementWithEmptyBody
346 while(iterator.next().getKey() != textRange);
348 final RangeMarker rangeToProcess = document.createRangeMarker(textRange.getEndOffset(), accumulatedRange.getEndOffset());
349 freeFormatingActions.add(new Pair<RangeMarker, ReformatAction>(rangeToProcess, new ReformatWithHeadingWhitespaceAction()));
350 accumulatedRange = textRange;
351 accumulatedRangeAction = action;
352 iterator.remove();
354 else {
355 if (!(accumulatedRangeAction instanceof ReindentAction)) {
356 iterator.remove();
357 if(accumulatedRangeAction instanceof ReformatAction && action instanceof ReformatWithHeadingWhitespaceAction &&
358 accumulatedRange.getStartOffset() == textRange.getStartOffset() ||
359 accumulatedRangeAction instanceof ReformatWithHeadingWhitespaceAction && action instanceof ReformatAction &&
360 accumulatedRange.getStartOffset() < textRange.getStartOffset()){
361 accumulatedRangeAction = action;
363 accumulatedRange = document.createRangeMarker(Math.min(accumulatedRange.getStartOffset(), textRange.getStartOffset()),
364 Math.max(accumulatedRange.getEndOffset(), textRange.getEndOffset()));
366 else if(action instanceof ReindentAction) iterator.remove(); // TODO[ik]: need to be fixed to correctly process indent inside indent
369 if (accumulatedRange != null){
370 if (accumulatedRangeAction instanceof ReindentAction)
371 indentActions.add(new Pair<RangeMarker, ReindentAction>(accumulatedRange, (ReindentAction)accumulatedRangeAction));
372 else
373 freeFormatingActions.add(new Pair<RangeMarker, ReformatAction>(accumulatedRange, (ReformatAction)accumulatedRangeAction));
376 final List<Pair<RangeMarker, ? extends PostponedAction>> result =
377 new ArrayList<Pair<RangeMarker, ? extends PostponedAction>>(rangesToProcess.size());
378 Collections.reverse(freeFormatingActions);
379 Collections.reverse(indentActions);
380 result.addAll(freeFormatingActions);
381 result.addAll(indentActions);
382 return result;
385 private boolean canStickActionsTogether(final PostponedAction currentAction,
386 final RangeMarker currentRange,
387 final PostponedAction nextAction,
388 final RangeMarker nextRange) {
389 // empty reformat markers can't sticked together with any action
390 if(nextAction instanceof ReformatWithHeadingWhitespaceAction && nextRange.getStartOffset() == nextRange.getEndOffset()) return false;
391 if(currentAction instanceof ReformatWithHeadingWhitespaceAction && currentRange.getStartOffset() == currentRange.getEndOffset()) return false;
392 // reindent actions can't be sticked at all
393 return !(currentAction instanceof ReindentAction);
396 private void createActionsMap(final List<ASTNode> astNodes, final FileViewProvider provider,
397 final TreeMap<RangeMarker, PostponedAction> rangesToProcess) {
398 final Set<ASTNode> nodesToProcess = new HashSet<ASTNode>(astNodes);
399 final Document document = provider.getDocument();
400 for (final ASTNode node : astNodes) {
401 nodesToProcess.remove(node);
402 final FileElement fileElement = TreeUtil.getFileElement((TreeElement)node);
403 if (fileElement == null || ((PsiFile)fileElement.getPsi()).getViewProvider() != provider) continue;
404 final boolean isGenerated = CodeEditUtil.isNodeGenerated(node);
406 ((TreeElement)node).acceptTree(new RecursiveTreeElementVisitor() {
407 boolean inGeneratedContext = !isGenerated;
408 protected boolean visitNode(TreeElement element) {
409 if(nodesToProcess.contains(element)) return false;
410 final boolean currentNodeGenerated = CodeEditUtil.isNodeGenerated(element);
411 CodeEditUtil.setNodeGenerated(element, false);
412 if(currentNodeGenerated && !inGeneratedContext){
413 rangesToProcess.put(document.createRangeMarker(element.getTextRange()), new ReformatAction());
414 inGeneratedContext = true;
416 if(!currentNodeGenerated && inGeneratedContext){
417 if(element.getElementType() == TokenType.WHITE_SPACE) return false;
418 final int oldIndent = CodeEditUtil.getOldIndentation(element);
419 LOG.assertTrue(oldIndent >= 0, "for not generated items old indentation must be defined");
420 rangesToProcess.put(document.createRangeMarker(element.getTextRange()), new ReindentAction(oldIndent));
421 inGeneratedContext = false;
423 return true;
426 @Override public void visitComposite(CompositeElement composite) {
427 boolean oldGeneratedContext = inGeneratedContext;
428 super.visitComposite(composite);
429 inGeneratedContext = oldGeneratedContext;
432 @Override public void visitLeaf(LeafElement leaf) {
433 boolean oldGeneratedContext = inGeneratedContext;
434 super.visitLeaf(leaf);
435 inGeneratedContext = oldGeneratedContext;
441 private void handleReformatMarkers(final FileViewProvider key,
442 final TreeMap<RangeMarker, PostponedAction> rangesToProcess) {
443 final Document document = key.getDocument();
444 for (final FileElement fileElement : ((SingleRootFileViewProvider)key).getKnownTreeRoots()) {
445 fileElement.acceptTree(
446 new RecursiveTreeElementWalkingVisitor(){
447 protected boolean visitNode(TreeElement element) {
448 if(CodeEditUtil.isMarkedToReformatBefore(element)) {
449 CodeEditUtil.markToReformatBefore(element, false);
450 rangesToProcess.put(document.createRangeMarker(element.getStartOffset(), element.getStartOffset()),
451 new ReformatWithHeadingWhitespaceAction());
453 return true;
459 private static void adjustIndentationInRange(final PsiFile file, final Document document, final TextRange[] indents, final int indentAdjustment) {
460 final Helper formatHelper = HelperFactory.createHelper(file.getFileType(), file.getProject());
461 final CharSequence charsSequence = document.getCharsSequence();
462 for (final TextRange indent : indents) {
463 final String oldIndentStr = charsSequence.subSequence(indent.getStartOffset() + 1, indent.getEndOffset()).toString();
464 final int oldIndent = formatHelper.getIndent(oldIndentStr, true);
465 final String newIndentStr = formatHelper.fillIndent(Math.max(oldIndent + indentAdjustment, 0));
466 document.replaceString(indent.getStartOffset() + 1, indent.getEndOffset(), newIndentStr);
470 private static int getNewIndent(final PsiFile psiFile, final int firstWhitespace) {
471 final Helper formatHelper = HelperFactory.createHelper(psiFile.getFileType(), psiFile.getProject());
472 final Document document = psiFile.getViewProvider().getDocument();
473 final int startOffset = document.getLineStartOffset(document.getLineNumber(firstWhitespace));
474 int endOffset = startOffset;
475 final CharSequence charsSequence = document.getCharsSequence();
476 while(Character.isWhitespace(charsSequence.charAt(endOffset++)));
477 final String newIndentStr = charsSequence.subSequence(startOffset, endOffset - 1).toString();
478 return formatHelper.getIndent(newIndentStr, true);
481 public boolean isDisabled() {
482 return myDisabledCounter > 0;
485 private CodeFormatterFacade getFormatterFacade(final FileViewProvider viewProvider) {
486 final CodeStyleSettings styleSettings = CodeStyleSettingsManager.getSettings(myPsiManager.getProject());
487 final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myPsiManager.getProject());
488 final Document document = viewProvider.getDocument();
489 final FileType fileType = viewProvider.getVirtualFile().getFileType();
490 final Helper helper = HelperFactory.createHelper(fileType, myPsiManager.getProject());
491 final CodeFormatterFacade codeFormatter = new CodeFormatterFacade(styleSettings, helper);
493 documentManager.commitDocument(document);
494 return codeFormatter;
497 private interface PostponedAction {
498 void processRange(RangeMarker marker, final FileViewProvider viewProvider);
501 private class ReformatAction implements PostponedAction {
502 public void processRange(RangeMarker marker, final FileViewProvider viewProvider) {
503 final CodeFormatterFacade codeFormatter = getFormatterFacade(viewProvider);
504 codeFormatter.processTextWithoutHeadWhitespace(viewProvider.getPsi(viewProvider.getBaseLanguage()),
505 marker.getStartOffset(), marker.getEndOffset());
509 private class ReformatWithHeadingWhitespaceAction extends ReformatAction{
510 public void processRange(RangeMarker marker, final FileViewProvider viewProvider) {
511 final CodeFormatterFacade codeFormatter = getFormatterFacade(viewProvider);
512 codeFormatter.processText(viewProvider.getPsi(viewProvider.getBaseLanguage()), marker.getStartOffset(),
513 marker.getStartOffset() == marker.getEndOffset() ? marker.getEndOffset() + 1: marker.getEndOffset());
519 private static class ReindentAction implements PostponedAction {
520 private final int myOldIndent;
522 public ReindentAction(final int oldIndent) {
523 myOldIndent = oldIndent;
526 public void processRange(RangeMarker marker, final FileViewProvider viewProvider) {
527 final Document document = viewProvider.getDocument();
528 final PsiFile psiFile = viewProvider.getPsi(viewProvider.getBaseLanguage());
529 final CharSequence charsSequence = document.getCharsSequence().subSequence(marker.getStartOffset(),
530 marker.getEndOffset());
531 final int oldIndent = getOldIndent();
532 final TextRange[] whitespaces = CharArrayUtil.getIndents(charsSequence, marker.getStartOffset());
533 final int indentAdjustment = getNewIndent(psiFile, marker.getStartOffset()) - oldIndent;
534 if(indentAdjustment != 0)
535 adjustIndentationInRange(psiFile, document, whitespaces, indentAdjustment);
538 private int getOldIndent() {
539 return myOldIndent;
544 public void projectOpened() {
547 public void projectClosed() {
550 @NotNull @NonNls
551 public String getComponentName() {
552 return "Postponed reformatting model";
555 public void initComponent() {
558 public void disposeComponent() {