blob: f99d47201a20234db12a663f467078cf50c9c54b [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.psi.impl.source;
import com.intellij.formatting.FormatTextRanges;
import com.intellij.lang.ASTNode;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationAdapter;
import com.intellij.openapi.application.ApplicationListener;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.fileTypes.FileTypeManager;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.*;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.PomManager;
import com.intellij.pom.PomModelAspect;
import com.intellij.pom.event.PomModelEvent;
import com.intellij.pom.tree.TreeAspect;
import com.intellij.pom.tree.events.ChangeInfo;
import com.intellij.pom.tree.events.TreeChange;
import com.intellij.pom.tree.events.TreeChangeEvent;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.impl.PsiTreeDebugBuilder;
import com.intellij.psi.impl.source.codeStyle.CodeEditUtil;
import com.intellij.psi.impl.source.codeStyle.CodeFormatterFacade;
import com.intellij.psi.impl.source.codeStyle.IndentHelperImpl;
import com.intellij.psi.impl.source.tree.*;
import com.intellij.util.LocalTimeCounter;
import com.intellij.util.containers.ContainerUtilRt;
import com.intellij.util.text.CharArrayUtil;
import com.intellij.util.text.TextRangeUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;
import java.util.*;
public class PostprocessReformattingAspect implements PomModelAspect {
private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.PostprocessReformattingAspect");
private final Project myProject;
private final PsiManager myPsiManager;
private final TreeAspect myTreeAspect;
private static final Key<Throwable> REFORMAT_ORIGINATOR = Key.create("REFORMAT_ORIGINATOR");
private static final boolean STORE_REFORMAT_ORIGINATOR_STACKTRACE = ApplicationManager.getApplication().isInternal();
private final ThreadLocal<Context> myContext = new ThreadLocal<Context>() {
@Override
protected Context initialValue() {
return new Context();
}
};
public PostprocessReformattingAspect(Project project, PsiManager psiManager, TreeAspect treeAspect,final CommandProcessor processor) {
myProject = project;
myPsiManager = psiManager;
myTreeAspect = treeAspect;
PomManager.getModel(psiManager.getProject())
.registerAspect(PostprocessReformattingAspect.class, this, Collections.singleton((PomModelAspect)treeAspect));
ApplicationListener applicationListener = new ApplicationAdapter() {
@Override
public void writeActionStarted(final Object action) {
if (processor != null) {
final Project project = processor.getCurrentCommandProject();
if (project == myProject) {
incrementPostponedCounter();
}
}
}
@Override
public void writeActionFinished(final Object action) {
if (processor != null) {
final Project project = processor.getCurrentCommandProject();
if (project == myProject) {
decrementPostponedCounter();
}
}
}
};
ApplicationManager.getApplication().addApplicationListener(applicationListener, project);
}
public void disablePostprocessFormattingInside(@NotNull final Runnable runnable) {
disablePostprocessFormattingInside(new NullableComputable<Object>() {
@Override
public Object compute() {
runnable.run();
return null;
}
});
}
public <T> T disablePostprocessFormattingInside(@NotNull Computable<T> computable) {
try {
getContext().myDisabledCounter++;
return computable.compute();
}
finally {
getContext().myDisabledCounter--;
LOG.assertTrue(getContext().myDisabledCounter > 0 || !isDisabled());
}
}
public void postponeFormattingInside(@NotNull final Runnable runnable) {
postponeFormattingInside(new NullableComputable<Object>() {
@Override
public Object compute() {
runnable.run();
return null;
}
});
}
public <T> T postponeFormattingInside(@NotNull Computable<T> computable) {
Application application = ApplicationManager.getApplication();
application.assertIsDispatchThread();
try {
incrementPostponedCounter();
return computable.compute();
}
finally {
decrementPostponedCounter();
}
}
private void incrementPostponedCounter() {
getContext().myPostponedCounter++;
}
private void decrementPostponedCounter() {
Application application = ApplicationManager.getApplication();
application.assertIsDispatchThread();
if (--getContext().myPostponedCounter == 0) {
if (application.isWriteAccessAllowed()) {
doPostponedFormatting();
}
else {
application.runWriteAction(new Runnable() {
@Override
public void run() {
doPostponedFormatting();
}
});
}
}
}
private static void atomic(@NotNull Runnable r) {
ProgressManager.getInstance().executeNonCancelableSection(r);
}
@Override
public void update(@NotNull final PomModelEvent event) {
atomic(new Runnable() {
@Override
public void run() {
if (isDisabled() || getContext().myPostponedCounter == 0 && !ApplicationManager.getApplication().isUnitTestMode()) return;
final TreeChangeEvent changeSet = (TreeChangeEvent)event.getChangeSet(myTreeAspect);
if (changeSet == null) return;
final PsiElement psiElement = changeSet.getRootElement().getPsi();
if (psiElement == null) return;
PsiFile containingFile = InjectedLanguageManager.getInstance(psiElement.getProject()).getTopLevelFile(psiElement);
final FileViewProvider viewProvider = containingFile.getViewProvider();
if (!viewProvider.isEventSystemEnabled()) return;
getContext().myUpdatedProviders.add(viewProvider);
for (final ASTNode node : changeSet.getChangedElements()) {
final TreeChange treeChange = changeSet.getChangesByElement(node);
for (final ASTNode affectedChild : treeChange.getAffectedChildren()) {
final ChangeInfo childChange = treeChange.getChangeByChild(affectedChild);
switch (childChange.getChangeType()) {
case ChangeInfo.ADD:
case ChangeInfo.REPLACE:
postponeFormatting(viewProvider, affectedChild);
break;
case ChangeInfo.CONTENTS_CHANGED:
if (!CodeEditUtil.isNodeGenerated(affectedChild)) {
((TreeElement)affectedChild).acceptTree(new RecursiveTreeElementWalkingVisitor() {
@Override
protected void visitNode(TreeElement element) {
if (CodeEditUtil.isNodeGenerated(element) && CodeEditUtil.isSuspendedNodesReformattingAllowed()) {
postponeFormatting(viewProvider, element);
return;
}
super.visitNode(element);
}
});
}
break;
}
}
}
}
});
}
public void doPostponedFormatting() {
atomic(new Runnable() {
@Override
public void run() {
if (isDisabled()) return;
try {
FileViewProvider[] viewProviders = getContext().myUpdatedProviders.toArray(new FileViewProvider[getContext().myUpdatedProviders.size()]);
for (final FileViewProvider viewProvider : viewProviders) {
doPostponedFormatting(viewProvider);
}
}
catch (Exception e) {
LOG.error(e);
}
finally {
LOG.assertTrue(getContext().myReformatElements.isEmpty(), getContext().myReformatElements);
}
}
});
}
public void postponedFormatting(@NotNull FileViewProvider viewProvider) {
postponedFormattingImpl(viewProvider, true);
}
public void doPostponedFormatting(@NotNull FileViewProvider viewProvider) {
postponedFormattingImpl(viewProvider, false);
}
private void postponedFormattingImpl(@NotNull final FileViewProvider viewProvider, final boolean check) {
atomic(new Runnable() {
@Override
public void run() {
if (isDisabled() || check && !getContext().myUpdatedProviders.contains(viewProvider)) return;
try {
disablePostprocessFormattingInside(new Runnable() {
@Override
public void run() {
doPostponedFormattingInner(viewProvider);
}
});
}
finally {
getContext().myUpdatedProviders.remove(viewProvider);
getContext().myReformatElements.remove(viewProvider);
viewProvider.putUserData(REFORMAT_ORIGINATOR, null);
}
}
});
}
public boolean isViewProviderLocked(@NotNull FileViewProvider fileViewProvider) {
return getContext().myReformatElements.containsKey(fileViewProvider);
}
public void beforeDocumentChanged(@NotNull FileViewProvider viewProvider) {
if (isViewProviderLocked(viewProvider)) {
Throwable cause = viewProvider.getUserData(REFORMAT_ORIGINATOR);
@NonNls String message = "Document is locked by write PSI operations. " +
"Use PsiDocumentManager.doPostponedOperationsAndUnblockDocument() to commit PSI changes to the document." +
(cause == null ? "" : " See cause stacktrace for the reason to lock.");
throw cause == null ? new RuntimeException(message): new RuntimeException(message, cause);
}
postponedFormatting(viewProvider);
}
public static PostprocessReformattingAspect getInstance(Project project) {
return project.getComponent(PostprocessReformattingAspect.class);
}
private void postponeFormatting(@NotNull FileViewProvider viewProvider, @NotNull ASTNode child) {
if (!CodeEditUtil.isNodeGenerated(child) && child.getElementType() != TokenType.WHITE_SPACE) {
final int oldIndent = CodeEditUtil.getOldIndentation(child);
LOG.assertTrue(oldIndent >= 0,
"for not generated items old indentation must be defined: element=" + child + ", text=" + child.getText());
}
List<ASTNode> list = getContext().myReformatElements.get(viewProvider);
if (list == null) {
list = new ArrayList<ASTNode>();
getContext().myReformatElements.put(viewProvider, list);
if (STORE_REFORMAT_ORIGINATOR_STACKTRACE) {
viewProvider.putUserData(REFORMAT_ORIGINATOR, new Throwable());
}
}
list.add(child);
}
private void doPostponedFormattingInner(@NotNull FileViewProvider key) {
final List<ASTNode> astNodes = getContext().myReformatElements.remove(key);
final Document document = key.getDocument();
// Sort ranges by end offsets so that we won't need any offset adjustment after reformat or reindent
if (document == null) return;
final VirtualFile virtualFile = key.getVirtualFile();
if (!virtualFile.isValid()) return;
final TreeSet<PostprocessFormattingTask> postProcessTasks = new TreeSet<PostprocessFormattingTask>();
Collection<Disposable> toDispose = ContainerUtilRt.newArrayList();
try {
// process all roots in viewProvider to find marked for reformat before elements and create appropriate range markers
handleReformatMarkers(key, postProcessTasks);
toDispose.addAll(postProcessTasks);
// then we create ranges by changed nodes. One per node. There ranges can intersect. Ranges are sorted by end offset.
if (astNodes != null) createActionsMap(astNodes, key, postProcessTasks);
if (Boolean.getBoolean("check.psi.is.valid") && ApplicationManager.getApplication().isUnitTestMode()) {
checkPsiIsCorrect(key);
}
while (!postProcessTasks.isEmpty()) {
// now we have to normalize actions so that they not intersect and ordered in most appropriate way
// (free reformatting -> reindent -> formatting under reindent)
final List<PostponedAction> normalizedActions = normalizeAndReorderPostponedActions(postProcessTasks, document);
toDispose.addAll(normalizedActions);
// only in following loop real changes in document are made
for (final PostponedAction normalizedAction : normalizedActions) {
CodeStyleSettings settings = CodeStyleSettingsManager.getSettings(myPsiManager.getProject());
boolean old = settings.ENABLE_JAVADOC_FORMATTING;
settings.ENABLE_JAVADOC_FORMATTING = false;
try {
normalizedAction.execute(key);
}
finally {
settings.ENABLE_JAVADOC_FORMATTING = old;
}
}
}
}
finally {
for (Disposable disposable : toDispose) {
//noinspection SSBasedInspection
disposable.dispose();
}
}
}
private void checkPsiIsCorrect(@NotNull FileViewProvider key) {
PsiFile actualPsi = key.getPsi(key.getBaseLanguage());
PsiTreeDebugBuilder treeDebugBuilder = new PsiTreeDebugBuilder().setShowErrorElements(false).setShowWhiteSpaces(false);
String actualPsiTree = treeDebugBuilder.psiToString(actualPsi);
String fileName = key.getVirtualFile().getName();
PsiFile psi = PsiFileFactory.getInstance(myProject)
.createFileFromText(fileName, FileTypeManager.getInstance().getFileTypeByFileName(fileName), actualPsi.getNode().getText(),
LocalTimeCounter.currentTime(), false);
if (actualPsi.getClass().equals(psi.getClass())) {
String expectedPsi = treeDebugBuilder.psiToString(psi);
if (!expectedPsi.equals(actualPsiTree)) {
getContext().myReformatElements.clear();
assert expectedPsi.equals(actualPsiTree) : "Refactored psi should be the same as result of parsing";
}
}
}
@NotNull
private List<PostponedAction> normalizeAndReorderPostponedActions(@NotNull Set<PostprocessFormattingTask> rangesToProcess, @NotNull Document document) {
final List<PostprocessFormattingTask> freeFormattingActions = new ArrayList<PostprocessFormattingTask>();
final List<ReindentTask> indentActions = new ArrayList<ReindentTask>();
PostprocessFormattingTask accumulatedTask = null;
Iterator<PostprocessFormattingTask> iterator = rangesToProcess.iterator();
while (iterator.hasNext()) {
final PostprocessFormattingTask currentTask = iterator.next();
if (accumulatedTask == null) {
accumulatedTask = currentTask;
iterator.remove();
}
else if (accumulatedTask.getStartOffset() > currentTask.getEndOffset() ||
accumulatedTask.getStartOffset() == currentTask.getEndOffset() &&
!canStickActionsTogether(accumulatedTask, currentTask)) {
// action can be pushed
if (accumulatedTask instanceof ReindentTask) {
indentActions.add((ReindentTask) accumulatedTask);
}
else {
freeFormattingActions.add(accumulatedTask);
}
accumulatedTask = currentTask;
iterator.remove();
}
else if (accumulatedTask instanceof ReformatTask && currentTask instanceof ReindentTask) {
// split accumulated reformat range into two
if (accumulatedTask.getStartOffset() < currentTask.getStartOffset()) {
final RangeMarker endOfRange = document.createRangeMarker(accumulatedTask.getStartOffset(), currentTask.getStartOffset());
// add heading reformat part
rangesToProcess.add(new ReformatTask(endOfRange));
// and manage heading whitespace because formatter does not edit it in previous action
iterator = rangesToProcess.iterator();
//noinspection StatementWithEmptyBody
while (iterator.next().getRange() != currentTask.getRange()) ;
}
final RangeMarker rangeToProcess = document.createRangeMarker(currentTask.getEndOffset(), accumulatedTask.getEndOffset());
freeFormattingActions.add(new ReformatWithHeadingWhitespaceTask(rangeToProcess));
accumulatedTask = currentTask;
iterator.remove();
}
else {
if (!(accumulatedTask instanceof ReindentTask)) {
iterator.remove();
boolean withLeadingWhitespace = accumulatedTask instanceof ReformatWithHeadingWhitespaceTask;
if (accumulatedTask instanceof ReformatTask &&
currentTask instanceof ReformatWithHeadingWhitespaceTask &&
accumulatedTask.getStartOffset() == currentTask.getStartOffset()) {
withLeadingWhitespace = true;
}
else if (accumulatedTask instanceof ReformatWithHeadingWhitespaceTask &&
currentTask instanceof ReformatTask &&
accumulatedTask.getStartOffset() < currentTask.getStartOffset()) {
withLeadingWhitespace = false;
}
int newStart = Math.min(accumulatedTask.getStartOffset(), currentTask.getStartOffset());
int newEnd = Math.max(accumulatedTask.getEndOffset(), currentTask.getEndOffset());
RangeMarker rangeMarker;
if (accumulatedTask.getStartOffset() == newStart && accumulatedTask.getEndOffset() == newEnd) {
rangeMarker = accumulatedTask.getRange();
}
else if (currentTask.getStartOffset() == newStart && currentTask.getEndOffset() == newEnd) {
rangeMarker = currentTask.getRange();
}
else {
rangeMarker = document.createRangeMarker(newStart, newEnd);
}
if (withLeadingWhitespace) {
accumulatedTask = new ReformatWithHeadingWhitespaceTask(rangeMarker);
}
else {
accumulatedTask = new ReformatTask(rangeMarker);
}
}
else if (currentTask instanceof ReindentTask) {
iterator.remove();
} // TODO[ik]: need to be fixed to correctly process indent inside indent
}
}
if (accumulatedTask != null) {
if (accumulatedTask instanceof ReindentTask) {
indentActions.add((ReindentTask) accumulatedTask);
}
else {
freeFormattingActions.add(accumulatedTask);
}
}
final List<PostponedAction> result = new ArrayList<PostponedAction>();
Collections.reverse(freeFormattingActions);
Collections.reverse(indentActions);
if (!freeFormattingActions.isEmpty()) {
FormatTextRanges ranges = new FormatTextRanges();
for (PostprocessFormattingTask action : freeFormattingActions) {
TextRange range = TextRange.create(action);
ranges.add(range, action instanceof ReformatWithHeadingWhitespaceTask);
}
result.add(new ReformatRangesAction(ranges));
}
if (!indentActions.isEmpty()) {
ReindentRangesAction reindentRangesAction = new ReindentRangesAction();
for (ReindentTask action : indentActions) {
reindentRangesAction.add(action.getRange(), action.getOldIndent());
}
result.add(reindentRangesAction);
}
return result;
}
private static boolean canStickActionsTogether(final PostprocessFormattingTask currentTask,
final PostprocessFormattingTask nextTask) {
// empty reformat markers can't be stuck together with any action
if (nextTask instanceof ReformatWithHeadingWhitespaceTask && nextTask.getStartOffset() == nextTask.getEndOffset()) return false;
if (currentTask instanceof ReformatWithHeadingWhitespaceTask && currentTask.getStartOffset() == currentTask.getEndOffset()) {
return false;
}
// reindent actions can't be be stuck at all
return !(currentTask instanceof ReindentTask);
}
private static void createActionsMap(@NotNull List<ASTNode> astNodes,
@NotNull FileViewProvider provider,
@NotNull final TreeSet<PostprocessFormattingTask> rangesToProcess) {
final Set<ASTNode> nodesToProcess = new HashSet<ASTNode>(astNodes);
final Document document = provider.getDocument();
if (document == null) {
return;
}
for (final ASTNode node : astNodes) {
nodesToProcess.remove(node);
final FileElement fileElement = TreeUtil.getFileElement((TreeElement)node);
if (fileElement == null || ((PsiFile)fileElement.getPsi()).getViewProvider() != provider) continue;
final boolean isGenerated = CodeEditUtil.isNodeGenerated(node);
((TreeElement)node).acceptTree(new RecursiveTreeElementVisitor() {
boolean inGeneratedContext = !isGenerated;
@Override
protected boolean visitNode(TreeElement element) {
if (nodesToProcess.contains(element)) return false;
final boolean currentNodeGenerated = CodeEditUtil.isNodeGenerated(element);
CodeEditUtil.setNodeGenerated(element, false);
if (currentNodeGenerated && !inGeneratedContext) {
rangesToProcess.add(new ReformatTask(document.createRangeMarker(element.getTextRange())));
inGeneratedContext = true;
}
if (!currentNodeGenerated && inGeneratedContext) {
if (element.getElementType() == TokenType.WHITE_SPACE) return false;
final int oldIndent = CodeEditUtil.getOldIndentation(element);
CodeEditUtil.setOldIndentation(element, -1);
LOG.assertTrue(oldIndent >= 0, "for not generated items old indentation must be defined: element " + element);
for (TextRange indentRange : getEnabledRanges(element.getPsi())) {
rangesToProcess.add(new ReindentTask(document.createRangeMarker(indentRange), oldIndent));
}
inGeneratedContext = false;
}
return true;
}
private Iterable<TextRange> getEnabledRanges(@NotNull PsiElement element) {
List<TextRange> disabledRanges = new ArrayList<TextRange>();
for (DisabledIndentRangesProvider rangesProvider : DisabledIndentRangesProvider.EP_NAME.getExtensions()) {
Collection<TextRange> providedDisabledRanges = rangesProvider.getDisabledIndentRanges(element);
if (providedDisabledRanges != null) {
disabledRanges.addAll(providedDisabledRanges);
}
}
return TextRangeUtil.excludeRanges(element.getTextRange(), disabledRanges);
}
@Override
public void visitComposite(CompositeElement composite) {
boolean oldGeneratedContext = inGeneratedContext;
super.visitComposite(composite);
inGeneratedContext = oldGeneratedContext;
}
@Override
public void visitLeaf(LeafElement leaf) {
boolean oldGeneratedContext = inGeneratedContext;
super.visitLeaf(leaf);
inGeneratedContext = oldGeneratedContext;
}
});
}
}
private static void handleReformatMarkers(@NotNull final FileViewProvider key, @NotNull final Set<PostprocessFormattingTask> rangesToProcess) {
final Document document = key.getDocument();
if (document == null) {
return;
}
for (final FileElement fileElement : ((SingleRootFileViewProvider)key).getKnownTreeRoots()) {
fileElement.acceptTree(new RecursiveTreeElementWalkingVisitor() {
@Override
protected void visitNode(TreeElement element) {
if (CodeEditUtil.isMarkedToReformatBefore(element)) {
CodeEditUtil.markToReformatBefore(element, false);
rangesToProcess.add(new ReformatWithHeadingWhitespaceTask(
document.createRangeMarker(element.getStartOffset(), element.getStartOffset()))
);
}
else if (CodeEditUtil.isMarkedToReformat(element)) {
CodeEditUtil.markToReformat(element, false);
rangesToProcess.add(new ReformatWithHeadingWhitespaceTask(
document.createRangeMarker(element.getStartOffset(), element.getStartOffset() + element.getTextLength()))
);
}
super.visitNode(element);
}
});
}
}
private static void adjustIndentationInRange(@NotNull PsiFile file,
@NotNull Document document,
@NotNull TextRange[] indents,
final int indentAdjustment) {
final CharSequence charsSequence = document.getCharsSequence();
for (final TextRange indent : indents) {
final String oldIndentStr = charsSequence.subSequence(indent.getStartOffset() + 1, indent.getEndOffset()).toString();
final int oldIndent = IndentHelperImpl.getIndent(file.getProject(), file.getFileType(), oldIndentStr, true);
final String newIndentStr = IndentHelperImpl
.fillIndent(file.getProject(), file.getFileType(), Math.max(oldIndent + indentAdjustment, 0));
document.replaceString(indent.getStartOffset() + 1, indent.getEndOffset(), newIndentStr);
}
}
private static int getNewIndent(@NotNull PsiFile psiFile, final int firstWhitespace) {
final Document document = psiFile.getViewProvider().getDocument();
assert document != null;
final int startOffset = document.getLineStartOffset(document.getLineNumber(firstWhitespace));
int endOffset = startOffset;
final CharSequence charsSequence = document.getCharsSequence();
//noinspection StatementWithEmptyBody
while (Character.isWhitespace(charsSequence.charAt(endOffset++))) ;
final String newIndentStr = charsSequence.subSequence(startOffset, endOffset - 1).toString();
return IndentHelperImpl.getIndent(psiFile.getProject(), psiFile.getFileType(), newIndentStr, true);
}
public boolean isDisabled() {
return getContext().myDisabledCounter > 0;
}
@NotNull
private CodeFormatterFacade getFormatterFacade(@NotNull FileViewProvider viewProvider) {
final CodeStyleSettings styleSettings = CodeStyleSettingsManager.getSettings(myPsiManager.getProject());
final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myPsiManager.getProject());
final Document document = viewProvider.getDocument();
assert document != null;
final CodeFormatterFacade codeFormatter = new CodeFormatterFacade(styleSettings, viewProvider.getBaseLanguage());
documentManager.commitDocument(document);
return codeFormatter;
}
private abstract static class PostprocessFormattingTask implements Comparable<PostprocessFormattingTask>, Segment, Disposable {
@NotNull private final RangeMarker myRange;
public PostprocessFormattingTask(@NotNull RangeMarker rangeMarker) {
myRange = rangeMarker;
}
@Override
public int compareTo(@NotNull PostprocessFormattingTask o) {
RangeMarker o1 = myRange;
RangeMarker o2 = o.myRange;
if (o1.equals(o2)) return 0;
final int diff = o2.getEndOffset() - o1.getEndOffset();
if (diff == 0) {
if (o1.getStartOffset() == o2.getStartOffset()) return 0;
if (o1.getStartOffset() == o1.getEndOffset()) return -1; // empty ranges first
if (o2.getStartOffset() == o2.getEndOffset()) return 1; // empty ranges first
return o1.getStartOffset() - o2.getStartOffset();
}
return diff;
}
@NotNull
public RangeMarker getRange() {
return myRange;
}
@Override
public int getStartOffset() {
return myRange.getStartOffset();
}
@Override
public int getEndOffset() {
return myRange.getEndOffset();
}
@Override
public void dispose() {
if (myRange.isValid()) {
myRange.dispose();
}
}
}
private static class ReformatTask extends PostprocessFormattingTask {
public ReformatTask(@NotNull RangeMarker rangeMarker) {
super(rangeMarker);
}
}
private static class ReformatWithHeadingWhitespaceTask extends PostprocessFormattingTask {
public ReformatWithHeadingWhitespaceTask(@NotNull RangeMarker rangeMarker) {
super(rangeMarker);
}
}
private static class ReindentTask extends PostprocessFormattingTask {
private final int myOldIndent;
public ReindentTask(@NotNull RangeMarker rangeMarker, int oldIndent) {
super(rangeMarker);
myOldIndent = oldIndent;
}
public int getOldIndent() {
return myOldIndent;
}
}
private interface PostponedAction extends Disposable {
void execute(@NotNull FileViewProvider viewProvider);
}
private class ReformatRangesAction implements PostponedAction {
private final FormatTextRanges myRanges;
public ReformatRangesAction(@NotNull FormatTextRanges ranges) {
myRanges = ranges;
}
@Override
public void execute(@NotNull FileViewProvider viewProvider) {
final CodeFormatterFacade codeFormatter = getFormatterFacade(viewProvider);
codeFormatter.processText(viewProvider.getPsi(viewProvider.getBaseLanguage()), myRanges.ensureNonEmpty(), false);
}
@Override
public void dispose() {
}
}
private static class ReindentRangesAction implements PostponedAction {
private final List<Pair<Integer, RangeMarker>> myRangesToReindent = new ArrayList<Pair<Integer, RangeMarker>>();
public void add(@NotNull RangeMarker rangeMarker, int oldIndent) {
myRangesToReindent.add(new Pair<Integer, RangeMarker>(oldIndent, rangeMarker));
}
@Override
public void execute(@NotNull FileViewProvider viewProvider) {
final Document document = viewProvider.getDocument();
assert document != null;
final PsiFile psiFile = viewProvider.getPsi(viewProvider.getBaseLanguage());
for (Pair<Integer, RangeMarker> integerRangeMarkerPair : myRangesToReindent) {
RangeMarker marker = integerRangeMarkerPair.second;
final CharSequence charsSequence = document.getCharsSequence().subSequence(marker.getStartOffset(), marker.getEndOffset());
final int oldIndent = integerRangeMarkerPair.first;
final TextRange[] whitespaces = CharArrayUtil.getIndents(charsSequence, marker.getStartOffset());
final int indentAdjustment = getNewIndent(psiFile, marker.getStartOffset()) - oldIndent;
if (indentAdjustment != 0) adjustIndentationInRange(psiFile, document, whitespaces, indentAdjustment);
}
}
@Override
public void dispose() {
for (Pair<Integer, RangeMarker> pair : myRangesToReindent) {
RangeMarker marker = pair.second;
if (marker.isValid()) {
marker.dispose();
}
}
}
}
@TestOnly
public void clear() {
getContext().myReformatElements.clear();
}
private Context getContext() {
return myContext.get();
}
private static class Context {
private int myPostponedCounter = 0;
private int myDisabledCounter = 0;
private final Set<FileViewProvider> myUpdatedProviders = new HashSet<FileViewProvider>();
private final Map<FileViewProvider, List<ASTNode>> myReformatElements = new HashMap<FileViewProvider, List<ASTNode>>();
}
}