Merge branch 'formatter-engine-refactoring'

This commit is contained in:
Yaroslav Lepenkin
2016-04-20 13:32:07 +03:00
28 changed files with 1939 additions and 1570 deletions
@@ -337,7 +337,7 @@ public abstract class AbstractBlockWrapper {
*
* @return object that encapsulates information about number of symbols before the current block
*/
protected abstract IndentData getNumberOfSymbolsBeforeBlock();
public abstract IndentData getNumberOfSymbolsBeforeBlock();
/**
* @return previous block for the current block if any; <code>null</code> otherwise
@@ -25,7 +25,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Set;
class AlignmentImpl extends Alignment {
public class AlignmentImpl extends Alignment {
private static final List<LeafBlockWrapper> EMPTY = Collections.emptyList();
private final boolean myAllowBackwardShift;
private final Anchor myAnchor;
@@ -110,7 +110,7 @@ class AlignmentImpl extends Alignment {
* {@link #setParent(Alignment) its parent} using the algorithm above if any; <code>null</code> otherwise
*/
@Nullable
LeafBlockWrapper getOffsetRespBlockBefore(@Nullable final AbstractBlockWrapper block) {
public LeafBlockWrapper getOffsetRespBlockBefore(@Nullable final AbstractBlockWrapper block) {
if (!continueOffsetResponsibleBlockRetrieval(block)) {
return null;
}
@@ -135,7 +135,7 @@ class AlignmentImpl extends Alignment {
*
* @param block wrapped block to register within the current alignment object
*/
void setOffsetRespBlock(final LeafBlockWrapper block) {
public void setOffsetRespBlock(final LeafBlockWrapper block) {
if (block == null) {
return;
}
@@ -75,7 +75,7 @@ public class CompositeBlockWrapper extends AbstractBlockWrapper{
}
@Override
protected IndentData getNumberOfSymbolsBeforeBlock() {
public IndentData getNumberOfSymbolsBeforeBlock() {
if (myChildren == null || myChildren.isEmpty()) {
return new IndentData(0, 0);
}
@@ -16,6 +16,7 @@
package com.intellij.formatting;
import com.intellij.formatting.engine.BlockRangesMap;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.containers.ContainerUtil;
@@ -88,14 +89,14 @@ public class DependantSpacingImpl extends SpacingImpl {
}
@Override
public void refresh(FormatProcessor formatter) {
public void refresh(BlockRangesMap helper) {
if (isDependentRegionLinefeedStatusChanged()) {
return;
}
boolean atLeastOneDependencyRangeContainsLf = false;
for (TextRange dependency : myDependentRegionRanges) {
atLeastOneDependencyRangeContainsLf |= formatter.containsLineFeeds(dependency);
atLeastOneDependencyRangeContainsLf |= helper.containsLineFeeds(dependency);
}
if (atLeastOneDependencyRangeContainsLf) myFlags |= DEPENDENCE_CONTAINS_LF_MASK;
File diff suppressed because it is too large Load Diff
@@ -16,6 +16,7 @@
package com.intellij.formatting;
import com.intellij.formatting.engine.ExpandableIndent;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
@@ -181,8 +182,6 @@ public class FormatterImpl extends FormatterEx
FormatProcessor processor = new FormatProcessor(
model.getDocumentModel(), model.getRootBlock(), settings, indentOptions, affectedRanges, FormattingProgressCallback.EMPTY
);
processor.setJavaIndentOptions(javaIndentOptions);
processor.format(model);
return processor;
}
@@ -306,21 +305,6 @@ public class FormatterImpl extends FormatterEx
execute(task);
}
/**
* Execute given sequential formatting task. Two approaches are possible:
* <pre>
* <ul>
* <li>
* <b>synchronous</b> - the task is completely executed during the current method processing;
* </li>
* <li>
* <b>asynchronous</b> - the task is executed at background thread under the progress dialog;
* </li>
* </ul>
* </pre>
*
* @param task task to execute
*/
private void execute(@NotNull SequentialTask task) {
disableFormatting();
Application application = ApplicationManager.getApplication();
@@ -363,7 +347,7 @@ public class FormatterImpl extends FormatterEx
final FormatProcessor processor = buildProcessorAndWrapBlocks(
model, block, settings, indentOptions, new FormatTextRanges(affectedRange, true)
);
final LeafBlockWrapper blockBefore = processor.getBlockAtOrAfter(affectedRange.getStartOffset());
final LeafBlockWrapper blockBefore = processor.getBlockRangesMap().getBlockAtOrAfter(affectedRange.getStartOffset());
LOG.assertTrue(blockBefore != null);
WhiteSpace whiteSpace = blockBefore.getWhiteSpace();
LOG.assertTrue(whiteSpace != null);
@@ -473,7 +457,7 @@ public class FormatterImpl extends FormatterEx
documentModel, block, settings, indentOptions, new FormatTextRanges(affectedRange, true), offset
);
final LeafBlockWrapper blockAfterOffset = processor.getBlockAtOrAfter(offset);
final LeafBlockWrapper blockAfterOffset = processor.getBlockRangesMap().getBlockAtOrAfter(offset);
if (blockAfterOffset != null && blockAfterOffset.contains(offset)) {
return offset;
@@ -492,40 +476,14 @@ public class FormatterImpl extends FormatterEx
return offset;
}
/**
* Delegates to
* {@link #buildProcessorAndWrapBlocks(FormattingDocumentModel, Block, CodeStyleSettings, CommonCodeStyleSettings.IndentOptions, FormatTextRanges, int)}
* with '-1' as an interested offset.
*
* @param docModel
* @param rootBlock
* @param settings
* @param indentOptions
* @param affectedRanges
* @return
*/
private static FormatProcessor buildProcessorAndWrapBlocks(final FormattingDocumentModel docModel,
Block rootBlock,
CodeStyleSettings settings,
CommonCodeStyleSettings.IndentOptions indentOptions,
@Nullable FormatTextRanges affectedRanges)
{
@Nullable FormatTextRanges affectedRanges) {
return buildProcessorAndWrapBlocks(docModel, rootBlock, settings, indentOptions, affectedRanges, -1);
}
/**
* Builds {@link FormatProcessor} instance and asks it to wrap all {@link Block code blocks}
* {@link FormattingModel#getRootBlock() derived from the given model}.
*
* @param docModel target model
* @param rootBlock root block to process
* @param settings code style settings to use
* @param indentOptions indent options to use
* @param affectedRanges ranges to reformat
* @param interestingOffset interesting offset; <code>'-1'</code> if no particular offset has a special interest
* @return format processor instance with wrapped {@link Block code blocks}
*/
@SuppressWarnings({"StatementWithEmptyBody"})
private static FormatProcessor buildProcessorAndWrapBlocks(final FormattingDocumentModel docModel,
Block rootBlock,
CodeStyleSettings settings,
@@ -599,7 +557,7 @@ public class FormatterImpl extends FormatterEx
final FormatProcessor processor = buildProcessorAndWrapBlocks(
documentModel, block, settings, indentOptions, new FormatTextRanges(affectedRange, true), offset
);
final LeafBlockWrapper blockAfterOffset = processor.getBlockAtOrAfter(offset);
final LeafBlockWrapper blockAfterOffset = processor.getBlockRangesMap().getBlockAtOrAfter(offset);
if (blockAfterOffset != null && !blockAfterOffset.contains(offset)) {
final WhiteSpace whiteSpace = blockAfterOffset.getWhiteSpace();
@@ -18,7 +18,7 @@ package com.intellij.formatting;
import org.jetbrains.annotations.NonNls;
class IndentImpl extends Indent {
public class IndentImpl extends Indent {
private final boolean myIsAbsolute;
private final boolean myRelativeToDirectParent;
@@ -21,7 +21,7 @@ import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class IndentInside {
public class IndentInside {
public int whiteSpaces = 0;
public int tabs = 0;
@@ -66,7 +66,7 @@ class IndentInside {
}
@NotNull
static IndentInside createIndentOn(@Nullable final CharSequence lastLine) {
public static IndentInside createIndentOn(@Nullable final CharSequence lastLine) {
final IndentInside result = new IndentInside();
if (lastLine == null) {
return result;
@@ -79,7 +79,7 @@ class IndentInside {
}
@NotNull
static CharSequence getLastLine(@NotNull final CharSequence text) {
public static CharSequence getLastLine(@NotNull final CharSequence text) {
int i = CharArrayUtil.shiftBackwardUntil(text, text.length() - 1, "\n");
if (i < 0) {
return text;
@@ -17,6 +17,7 @@
package com.intellij.formatting;
import com.intellij.diagnostic.LogMessageEx;
import com.intellij.formatting.engine.ExpandableIndent;
import com.intellij.lang.LanguageFormatting;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
@@ -46,7 +47,7 @@ import java.util.Set;
* Allows to build {@link AbstractBlockWrapper formatting block wrappers} for the target {@link Block formatting blocks}.
* The main idea of block wrapping is to associate information about {@link WhiteSpace white space before block} with the block itself.
*/
class InitialInfoBuilder {
public class InitialInfoBuilder {
private static final Logger LOG = Logger.getInstance("#com.intellij.formatting.InitialInfoBuilder");
private final Map<AbstractBlockWrapper, Block> myResult = new THashMap<AbstractBlockWrapper, Block>();
@@ -98,15 +99,16 @@ class InitialInfoBuilder {
myFormatterTagHandler = new FormatterTagHandler(settings);
}
protected static InitialInfoBuilder prepareToBuildBlocksSequentially(Block root,
FormattingDocumentModel model,
@Nullable final FormatTextRanges affectedRanges,
@NotNull CodeStyleSettings settings,
final CommonCodeStyleSettings.IndentOptions options,
int interestingOffset,
@NotNull FormattingProgressCallback progressCallback)
protected static InitialInfoBuilder prepareToBuildBlocksSequentially(
Block root,
FormattingDocumentModel model,
FormatProcessor.FormatOptions formatOptions,
CodeStyleSettings settings,
CommonCodeStyleSettings.IndentOptions options,
@NotNull FormattingProgressCallback progressCallback)
{
InitialInfoBuilder builder = new InitialInfoBuilder(root, model, affectedRanges, settings, options, interestingOffset, progressCallback);
InitialInfoBuilder builder = new InitialInfoBuilder(root, model, formatOptions.myAffectedRanges, settings, options, formatOptions.myInterestingOffset, progressCallback);
builder.setCollectAlignmentsInsideFormattingRange(formatOptions.myReformatContext);
builder.buildFrom(root, 0, null, null, null, true);
return builder;
}
@@ -120,8 +122,12 @@ class InitialInfoBuilder {
}
return minOffset;
}
public FormattingDocumentModel getFormattingDocumentModel() {
return myModel;
}
int getEndOffset() {
public int getEndOffset() {
int maxDocOffset = myModel.getTextLength();
int maxOffset = myRootBlockWrapper != null ? myRootBlockWrapper.getEndOffset() : 0;
if (myAffectedRanges != null) {
@@ -21,7 +21,7 @@ import com.intellij.openapi.util.TextRange;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import org.jetbrains.annotations.Nullable;
class LeafBlockWrapper extends AbstractBlockWrapper {
public class LeafBlockWrapper extends AbstractBlockWrapper {
private static final int CONTAIN_LINE_FEEDS = 4;
private static final int READ_ONLY = 8;
private static final int LEAF = 16;
@@ -126,7 +126,7 @@ class LeafBlockWrapper extends AbstractBlockWrapper {
}
@Override
protected IndentData getNumberOfSymbolsBeforeBlock() {
public IndentData getNumberOfSymbolsBeforeBlock() {
int spaces = getWhiteSpace().getSpaces();
int indentSpaces = getWhiteSpace().getIndentSpaces();
@@ -16,13 +16,14 @@
package com.intellij.formatting;
import com.intellij.formatting.engine.BlockRangesMap;
import org.jetbrains.annotations.NonNls;
/**
* Extends {@link Spacing} in order to keep number of additional settings like <code>'minSpaces'</code>, <code>'minLineFeeds'</code>,
* <code>'prefLineFeeds'</code> etc.
*/
class SpacingImpl extends Spacing {
public class SpacingImpl extends Spacing {
private int myMinSpaces;
private int myKeepBlankLines;
private int myMaxSpaces;
@@ -76,7 +77,7 @@ class SpacingImpl extends Spacing {
return myMinLineFeeds;
}
final boolean isReadOnly(){
public final boolean isReadOnly(){
return (myFlags & READ_ONLY_MASK) != 0;
}
@@ -90,10 +91,8 @@ class SpacingImpl extends Spacing {
/**
* Allows to ask to refresh current state using given formatter if necessary.
*
* @param formatter formatter to use during state refresh
*/
public void refresh(FormatProcessor formatter) {
public void refresh(BlockRangesMap helper) {
}
public final boolean shouldKeepLineFeeds() {
@@ -16,6 +16,7 @@
package com.intellij.formatting;
import com.intellij.formatting.engine.BlockRangesMap;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
@@ -45,7 +46,7 @@ import java.util.ArrayList;
* <p/>
* Not thread-safe.
*/
class WhiteSpace {
public class WhiteSpace {
private static final char LINE_FEED = '\n';
@@ -392,16 +393,13 @@ class WhiteSpace {
/**
* Tries to ensure that number of line feeds managed by the current {@link WhiteSpace} is consistent to the settings
* defined at the given spacing property.
*
* @param spaceProperty space settings holder
* @param formatProcessor format processor to use for space settings state refreshing
*/
public void arrangeLineFeeds(final SpacingImpl spaceProperty, final FormatProcessor formatProcessor) {
public void arrangeLineFeeds(final SpacingImpl spaceProperty, final BlockRangesMap helper) {
performModification(new Runnable() {
@Override
public void run() {
if (spaceProperty != null) {
spaceProperty.refresh(formatProcessor);
spaceProperty.refresh(helper);
if (spaceProperty.getMinLineFeeds() >= 0 && getLineFeeds() < spaceProperty.getMinLineFeeds()) {
setLineFeeds(spaceProperty.getMinLineFeeds());
@@ -541,11 +539,8 @@ class WhiteSpace {
* <p/>
* This method may be considered a shortcut for calling {@link #arrangeLineFeeds(SpacingImpl, FormatProcessor)} and
* {@link #arrangeSpaces(SpacingImpl)}.
*
* @param spacing spacing settings holder
* @param formatProcessor format processor to use to refresh state of the given <code>'spacing'</code> object
*/
public void removeLineFeeds(final SpacingImpl spacing, final FormatProcessor formatProcessor) {
public void removeLineFeeds(final SpacingImpl spacing, final BlockRangesMap helper) {
performModification(new Runnable() {
@Override
public void run() {
@@ -554,7 +549,7 @@ class WhiteSpace {
myIndentSpaces = 0;
}
});
arrangeLineFeeds(spacing, formatProcessor);
arrangeLineFeeds(spacing, helper);
arrangeSpaces(spacing);
}
@@ -20,7 +20,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.*;
class WrapImpl extends Wrap {
public class WrapImpl extends Wrap {
/**
* The block where the wrap needs to happen if the CHOP wrap mode is used and the chain of blocks exceeds the right margin.
*/
@@ -124,11 +124,11 @@ class WrapImpl extends Wrap {
myIgnoredWraps.get(wrap).add(currentBlock);
}
enum Type{
public enum Type{
DO_NOT_WRAP, WRAP_AS_NEEDED, CHOP_IF_NEEDED, WRAP_ALWAYS
}
LeafBlockWrapper getChopStartBlock() {
public LeafBlockWrapper getChopStartBlock() {
return myChopStartBlock;
}
@@ -139,7 +139,7 @@ class WrapImpl extends Wrap {
* <li>'{@link #isActive() isActive}' property value is set (to <code>true</code>)</li>
* </ul>
*/
void setActive() {
public void setActive() {
myChopStartBlock = null;
myFlags |= ACTIVE_MASK;
}
@@ -150,7 +150,7 @@ class WrapImpl extends Wrap {
*
* @param startOffset new '{@link #getWrapOffset() firstPosition}' property value to use if current value is undefined (negative)
*/
void setWrapOffset(final int startOffset) {
public void setWrapOffset(final int startOffset) {
if (myWrapOffset < 0) {
myWrapOffset = startOffset;
}
@@ -160,7 +160,7 @@ class WrapImpl extends Wrap {
* @return '{@link #getWrapOffset() firstPosition}' property value defined previously via {@link #setWrapOffset(int)} if any;
* <code>'-1'</code> otherwise
*/
int getWrapOffset() {
public int getWrapOffset() {
return myWrapOffset;
}
@@ -180,7 +180,7 @@ class WrapImpl extends Wrap {
myFlags |= (wrapFirstElement ? WRAP_FIRST_ELEMENT_MASK:0) | (myType.ordinal() << TYPE_SHIFT) | (myId << ID_SHIFT);
}
final Type getType() {
public final Type getType() {
return myTypes[(myFlags & TYPE_MASK) >>> TYPE_SHIFT];
}
@@ -190,17 +190,17 @@ class WrapImpl extends Wrap {
*
* @return <code>'wrapFirstElement'</code> property value
*/
final boolean isWrapFirstElement() {
public final boolean isWrapFirstElement() {
return (myFlags & WRAP_FIRST_ELEMENT_MASK) != 0;
}
void saveChopBlock(LeafBlockWrapper current) {
public void saveChopBlock(LeafBlockWrapper current) {
if (myChopStartBlock == null) {
myChopStartBlock = current;
}
}
final boolean isActive() {
public final boolean isActive() {
return (myFlags & ACTIVE_MASK) != 0;
}
@@ -0,0 +1,228 @@
/*
* Copyright 2000-2016 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.formatting.engine;
import com.intellij.formatting.*;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.TextRange;
import com.intellij.util.containers.ContainerUtil;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class AdjustWhiteSpacesState extends State {
private final FormattingProgressCallback myProgressCallback;
private WrapBlocksState myWrapBlocksState;
private LeafBlockWrapper myCurrentBlock;
private DependentSpacingEngine myDependentSpacingEngine;
private WrapProcessor myWrapProcessor;
private BlockRangesMap myBlockRangesMap;
private IndentAdjuster myIndentAdjuster;
private boolean myReformatContext;
private Set<Alignment> myAlignmentsInsideRangesToModify = null;
private final HashSet<WhiteSpace> myAlignAgain = new HashSet<WhiteSpace>();
private LeafBlockWrapper myFirstBlock;
public AdjustWhiteSpacesState(WrapBlocksState state,
FormattingProgressCallback progressCallback,
boolean isReformatContext) {
myWrapBlocksState = state;
myProgressCallback = progressCallback;
myReformatContext = isReformatContext;
}
@Override
public void prepare() {
if (myWrapBlocksState != null) {
myFirstBlock = myWrapBlocksState.getFirstBlock();
myCurrentBlock = myFirstBlock;
myDependentSpacingEngine = myWrapBlocksState.getDependentSpacingEngine();
myWrapProcessor = myWrapBlocksState.getWrapProcessor();
myIndentAdjuster = myWrapBlocksState.getIndentAdjuster();
myBlockRangesMap = myWrapBlocksState.getBlockRangesMap();
myAlignmentsInsideRangesToModify = myWrapBlocksState.getAlignmentsInsideRangesToModify();
}
}
public AdjustWhiteSpacesState(LeafBlockWrapper firstBlock,
DependentSpacingEngine dependentSpacingEngine,
WrapProcessor wrapProcessor,
IndentAdjuster indentAdjuster,
BlockRangesMap blockRangesMap,
Set<Alignment> alignmentsInsideRangesToModify,
boolean isReformatContext,
FormattingProgressCallback progressCallback)
{
myWrapBlocksState = null;
myCurrentBlock = firstBlock;
myDependentSpacingEngine = dependentSpacingEngine;
myWrapProcessor = wrapProcessor;
myIndentAdjuster = indentAdjuster;
myBlockRangesMap = blockRangesMap;
myAlignmentsInsideRangesToModify = alignmentsInsideRangesToModify;
myReformatContext = isReformatContext;
myProgressCallback = progressCallback;
}
public LeafBlockWrapper getCurrentBlock() {
return myCurrentBlock;
}
@Override
public void doIteration() {
LeafBlockWrapper blockToProcess = myCurrentBlock;
processToken();
if (blockToProcess != null) {
myProgressCallback.afterProcessingBlock(blockToProcess);
}
if (myCurrentBlock != null) {
return;
}
if (myAlignAgain.isEmpty()) {
setDone(true);
}
else {
myAlignAgain.clear();
myDependentSpacingEngine.clear();
myCurrentBlock = myFirstBlock;
}
}
private boolean isReformatSelectedRangesContext() {
return myReformatContext && !ContainerUtil.isEmpty(myAlignmentsInsideRangesToModify);
}
private void defineAlignOffset(final LeafBlockWrapper block) {
AbstractBlockWrapper current = myCurrentBlock;
while (true) {
final AlignmentImpl alignment = current.getAlignment();
if (alignment != null) {
alignment.setOffsetRespBlock(block);
}
current = current.getParent();
if (current == null) return;
if (current.getStartOffset() != myCurrentBlock.getStartOffset()) return;
}
}
private void onCurrentLineChanged() {
myWrapProcessor.onCurrentLineChanged();
}
private boolean isCurrentBlockAlignmentUsedInRangesToModify() {
AbstractBlockWrapper block = myCurrentBlock;
AlignmentImpl alignment = myCurrentBlock.getAlignment();
while (alignment == null) {
block = block.getParent();
if (block == null || block.getStartOffset() != myCurrentBlock.getStartOffset()) {
return false;
}
alignment = block.getAlignment();
}
return myAlignmentsInsideRangesToModify.contains(alignment);
}
private static List<TextRange> getDependentRegionRangesAfterCurrentWhiteSpace(final SpacingImpl spaceProperty,
final WhiteSpace whiteSpace) {
if (!(spaceProperty instanceof DependantSpacingImpl)) return ContainerUtil.emptyList();
if (whiteSpace.isReadOnly() || whiteSpace.isLineFeedsAreReadOnly()) return ContainerUtil.emptyList();
DependantSpacingImpl spacing = (DependantSpacingImpl)spaceProperty;
return ContainerUtil.filter(spacing.getDependentRegionRanges(), new Condition<TextRange>() {
@Override
public boolean value(TextRange dependencyRange) {
return whiteSpace.getStartOffset() < dependencyRange.getEndOffset();
}
});
}
private void processToken() {
final SpacingImpl spaceProperty = myCurrentBlock.getSpaceProperty();
final WhiteSpace whiteSpace = myCurrentBlock.getWhiteSpace();
if (isReformatSelectedRangesContext()) {
if (isCurrentBlockAlignmentUsedInRangesToModify() &&
whiteSpace.isReadOnly() &&
spaceProperty != null &&
!spaceProperty.isReadOnly()) {
whiteSpace.setReadOnly(false);
whiteSpace.setLineFeedsAreReadOnly(true);
}
}
whiteSpace.arrangeLineFeeds(spaceProperty, myBlockRangesMap);
if (!whiteSpace.containsLineFeeds()) {
whiteSpace.arrangeSpaces(spaceProperty);
}
try {
LeafBlockWrapper newBlock = myWrapProcessor.processWrap(myCurrentBlock);
if (newBlock != null) {
myCurrentBlock = newBlock;
return;
}
}
finally {
if (whiteSpace.containsLineFeeds()) {
onCurrentLineChanged();
}
}
LeafBlockWrapper newCurrentBlock = myIndentAdjuster.adjustIndent(myCurrentBlock);
if (newCurrentBlock != null) {
myCurrentBlock = newCurrentBlock;
onCurrentLineChanged();
return;
}
defineAlignOffset(myCurrentBlock);
if (myCurrentBlock.containsLineFeeds()) {
onCurrentLineChanged();
}
final List<TextRange> ranges = getDependentRegionRangesAfterCurrentWhiteSpace(spaceProperty, whiteSpace);
if (!ranges.isEmpty()) {
myDependentSpacingEngine.registerUnresolvedDependentSpacingRanges(spaceProperty, ranges);
}
if (!whiteSpace.isIsReadOnly() && myDependentSpacingEngine.shouldReformatPreviouslyLocatedDependentSpacing(whiteSpace)) {
myAlignAgain.add(whiteSpace);
}
else if (!myAlignAgain.isEmpty()) {
myAlignAgain.remove(whiteSpace);
}
myCurrentBlock = myCurrentBlock.getNextBlock();
}
}
@@ -0,0 +1,146 @@
/*
* Copyright 2000-2015 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.formatting.engine;
import com.intellij.diagnostic.LogMessageEx;
import com.intellij.formatting.*;
import com.intellij.lang.ASTNode;
import com.intellij.lang.Language;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class AlignmentHelper {
private static final Logger LOG = Logger.getInstance(AlignmentHelper.class);
private static final Map<Alignment.Anchor, BlockAlignmentProcessor> ALIGNMENT_PROCESSORS = new EnumMap<Alignment.Anchor, BlockAlignmentProcessor>(Alignment.Anchor.class);
static {
ALIGNMENT_PROCESSORS.put(Alignment.Anchor.LEFT, new LeftEdgeAlignmentProcessor());
ALIGNMENT_PROCESSORS.put(Alignment.Anchor.RIGHT, new RightEdgeAlignmentProcessor());
}
private final Set<Alignment> myAlignmentsToSkip = ContainerUtil.newHashSet();
private final Document myDocument;
private final BlockIndentOptions myBlockIndentOptions;
private final AlignmentCyclesDetector myCyclesDetector;
private final Map<LeafBlockWrapper, Set<LeafBlockWrapper>> myBackwardShiftedAlignedBlocks = ContainerUtil.newHashMap();
private final Map<AbstractBlockWrapper, Set<AbstractBlockWrapper>> myAlignmentMappings = ContainerUtil.newHashMap();
public AlignmentHelper(Document document, MultiMap<Alignment, Block> blocksToAlign, BlockIndentOptions options) {
myDocument = document;
myBlockIndentOptions = options;
int totalBlocks = blocksToAlign.values().size();
myCyclesDetector = new AlignmentCyclesDetector(totalBlocks);
}
private static void reportAlignmentProcessingError(BlockAlignmentProcessor.Context context) {
ASTNode node = context.targetBlock.getNode();
Language language = node != null ? node.getPsi().getLanguage() : null;
LogMessageEx.error(LOG,
(language != null ? language.getDisplayName() + ": " : "") +
"Can't align block " + context.targetBlock, context.document.getText());
}
public LeafBlockWrapper applyAlignment(final AlignmentImpl alignment, final LeafBlockWrapper currentBlock) {
BlockAlignmentProcessor alignmentProcessor = ALIGNMENT_PROCESSORS.get(alignment.getAnchor());
if (alignmentProcessor == null) {
LOG.error(String.format("Can't find alignment processor for alignment anchor %s", alignment.getAnchor()));
return null;
}
BlockAlignmentProcessor.Context context = new BlockAlignmentProcessor.Context(
myDocument, alignment, currentBlock, myAlignmentMappings, myBackwardShiftedAlignedBlocks,
myBlockIndentOptions.getIndentOptions(currentBlock));
final LeafBlockWrapper offsetResponsibleBlock = alignment.getOffsetRespBlockBefore(currentBlock);
myCyclesDetector.registerOffsetResponsibleBlock(offsetResponsibleBlock);
BlockAlignmentProcessor.Result result = alignmentProcessor.applyAlignment(context);
switch (result) {
case TARGET_BLOCK_PROCESSED_NOT_ALIGNED:
return null;
case TARGET_BLOCK_ALIGNED:
storeAlignmentMapping(currentBlock);
return null;
case BACKWARD_BLOCK_ALIGNED:
if (offsetResponsibleBlock == null) {
return null;
}
Set<LeafBlockWrapper> blocksCausedRealignment = new HashSet<LeafBlockWrapper>();
myBackwardShiftedAlignedBlocks.clear();
myBackwardShiftedAlignedBlocks.put(offsetResponsibleBlock, blocksCausedRealignment);
blocksCausedRealignment.add(currentBlock);
storeAlignmentMapping(currentBlock, offsetResponsibleBlock);
if (myCyclesDetector.isCycleDetected()) {
reportAlignmentProcessingError(context);
return null;
}
myCyclesDetector.registerBlockRollback(currentBlock);
return offsetResponsibleBlock.getNextBlock();
case RECURSION_DETECTED:
myAlignmentsToSkip.add(alignment);
return offsetResponsibleBlock; // Fall through to the 'register alignment to skip'.
case UNABLE_TO_ALIGN_BACKWARD_BLOCK:
myAlignmentsToSkip.add(alignment);
return null;
default:
return null;
}
}
public boolean shouldSkip(AlignmentImpl alignment) {
return myAlignmentsToSkip.contains(alignment);
}
private void storeAlignmentMapping(AbstractBlockWrapper block1, AbstractBlockWrapper block2) {
doStoreAlignmentMapping(block1, block2);
doStoreAlignmentMapping(block2, block1);
}
private void doStoreAlignmentMapping(AbstractBlockWrapper key, AbstractBlockWrapper value) {
Set<AbstractBlockWrapper> wrappers = myAlignmentMappings.get(key);
if (wrappers == null) {
myAlignmentMappings.put(key, wrappers = new HashSet<AbstractBlockWrapper>());
}
wrappers.add(value);
}
private void storeAlignmentMapping(LeafBlockWrapper currentBlock) {
AlignmentImpl alignment = null;
AbstractBlockWrapper block = currentBlock;
while (alignment == null && block != null) {
alignment = block.getAlignment();
block = block.getParent();
}
if (alignment != null) {
block = alignment.getOffsetRespBlockBefore(currentBlock);
if (block != null) {
storeAlignmentMapping(currentBlock, block);
}
}
}
public void reset() {
myBackwardShiftedAlignedBlocks.clear();
myAlignmentMappings.clear();
}
}
@@ -0,0 +1,225 @@
/*
* Copyright 2000-2015 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.formatting.engine;
import com.intellij.formatting.*;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.TextChange;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.editor.impl.BulkChangesMerger;
import com.intellij.openapi.editor.impl.TextChangeImpl;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
public class ApplyChangesState extends State {
/**
* There is a possible case that formatting introduced big number of changes to the underlying document. That number may be
* big enough for that their subsequent appliance is much slower than direct replacing of the whole document text.
* <p/>
* Current constant holds minimum number of changes that should trigger such <code>'replace whole text'</code> optimization.
*/
private static final int BULK_REPLACE_OPTIMIZATION_CRITERIA = 3000;
private final FormattingModel myModel;
private final FormattingProgressCallback myProgressCallback;
private final WrapBlocksState myWrapState;
private List<LeafBlockWrapper> myBlocksToModify;
private int myShift;
private int myIndex;
private boolean myResetBulkUpdateState;
private BlockIndentOptions myBlockIndentOptions;
public ApplyChangesState(FormattingModel model, WrapBlocksState state, FormattingProgressCallback callback) {
myModel = model;
myWrapState = state;
myProgressCallback = callback;
myBlockIndentOptions = state.getBlockIndentOptions();
}
/**
* Performs formatter changes in a series of blocks, for each block a new contents of document is calculated
* and whole document is replaced in one operation.
*
* @param blocksToModify changes introduced by formatter
* @param model current formatting model
*/
@SuppressWarnings({"deprecation"})
private void applyChangesAtRewriteMode(@NotNull final List<LeafBlockWrapper> blocksToModify,
@NotNull final FormattingModel model) {
FormattingDocumentModel documentModel = model.getDocumentModel();
Document document = documentModel.getDocument();
CaretOffsetUpdater caretOffsetUpdater = new CaretOffsetUpdater(document);
if (document instanceof DocumentEx) ((DocumentEx)document).setInBulkUpdate(true);
try {
List<TextChange> changes = new ArrayList<TextChange>();
int shift = 0;
int currentIterationShift = 0;
for (LeafBlockWrapper block : blocksToModify) {
WhiteSpace whiteSpace = block.getWhiteSpace();
CharSequence newWs = documentModel.adjustWhiteSpaceIfNecessary(
whiteSpace.generateWhiteSpace(myBlockIndentOptions.getIndentOptions(block)), whiteSpace.getStartOffset(),
whiteSpace.getEndOffset(), block.getNode(), false
);
if (changes.size() > 10000) {
caretOffsetUpdater.update(changes);
CharSequence mergeResult =
BulkChangesMerger.INSTANCE.mergeToCharSequence(document.getChars(), document.getTextLength(), changes);
document.replaceString(0, document.getTextLength(), mergeResult);
shift += currentIterationShift;
currentIterationShift = 0;
changes.clear();
}
TextChangeImpl change = new TextChangeImpl(newWs, whiteSpace.getStartOffset() + shift, whiteSpace.getEndOffset() + shift);
currentIterationShift += change.getDiff();
changes.add(change);
}
caretOffsetUpdater.update(changes);
CharSequence mergeResult = BulkChangesMerger.INSTANCE.mergeToCharSequence(document.getChars(), document.getTextLength(), changes);
document.replaceString(0, document.getTextLength(), mergeResult);
}
finally {
if (document instanceof DocumentEx) ((DocumentEx)document).setInBulkUpdate(false);
}
caretOffsetUpdater.restoreCaretLocations();
cleanupBlocks(blocksToModify);
}
private static void cleanupBlocks(List<LeafBlockWrapper> blocks) {
for (LeafBlockWrapper block : blocks) {
block.getParent().dispose();
block.dispose();
}
blocks.clear();
}
@Nullable
private static DocumentEx getAffectedDocument(final FormattingModel model) {
final Document document = model.getDocumentModel().getDocument();
if (document instanceof DocumentEx) {
return (DocumentEx)document;
}
else {
return null;
}
}
private List<LeafBlockWrapper> collectBlocksToModify() {
List<LeafBlockWrapper> blocksToModify = new ArrayList<LeafBlockWrapper>();
LeafBlockWrapper firstBlock = myWrapState.getFirstBlock();
for (LeafBlockWrapper block = firstBlock; block != null; block = block.getNextBlock()) {
final WhiteSpace whiteSpace = block.getWhiteSpace();
if (!whiteSpace.isReadOnly()) {
final String newWhiteSpace = whiteSpace.generateWhiteSpace(myBlockIndentOptions.getIndentOptions(block));
if (!whiteSpace.equalsToString(newWhiteSpace)) {
blocksToModify.add(block);
}
}
}
return blocksToModify;
}
@Override
public void prepare() {
myBlocksToModify = collectBlocksToModify();
// call doModifications static method to ensure no access to state
// thus we may clear formatting state
//reset();
//myDisposed = true;
if (myBlocksToModify.isEmpty()) {
setDone(true);
return;
}
myProgressCallback.beforeApplyingFormatChanges(myBlocksToModify);
final int blocksToModifyCount = myBlocksToModify.size();
if (blocksToModifyCount > BULK_REPLACE_OPTIMIZATION_CRITERIA) {
applyChangesAtRewriteMode(myBlocksToModify, myModel);
setDone(true);
}
else if (blocksToModifyCount > 50) {
DocumentEx updatedDocument = getAffectedDocument(myModel);
if (updatedDocument != null) {
updatedDocument.setInBulkUpdate(true);
myResetBulkUpdateState = true;
}
}
}
@Override
protected void doIteration() {
LeafBlockWrapper blockWrapper = myBlocksToModify.get(myIndex);
myShift = FormatProcessorUtils.replaceWhiteSpace(
myModel,
blockWrapper,
myShift,
blockWrapper.getWhiteSpace().generateWhiteSpace(myBlockIndentOptions.getIndentOptions(blockWrapper)),
myBlockIndentOptions.getIndentOptions()
);
myProgressCallback.afterApplyingChange(blockWrapper);
// block could be gc'd
blockWrapper.getParent().dispose();
blockWrapper.dispose();
myBlocksToModify.set(myIndex, null);
myIndex++;
if (myIndex >= myBlocksToModify.size()) {
setDone(true);
}
}
@Override
protected void setDone(boolean done) {
super.setDone(done);
if (myResetBulkUpdateState) {
DocumentEx document = getAffectedDocument(myModel);
if (document != null) {
document.setInBulkUpdate(false);
myResetBulkUpdateState = false;
}
}
if (done) {
myModel.commitChanges();
}
}
@Override
public void stop() {
if (myIndex > 0) {
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
myModel.commitChanges();
}
});
}
}
}
@@ -0,0 +1,78 @@
/*
* Copyright 2000-2015 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.formatting.engine;
import com.intellij.formatting.ASTBlock;
import com.intellij.formatting.AbstractBlockWrapper;
import com.intellij.formatting.Block;
import com.intellij.lang.ASTNode;
import com.intellij.lang.Language;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import org.jetbrains.annotations.NotNull;
public class BlockIndentOptions {
private final CodeStyleSettings mySettings;
private final CommonCodeStyleSettings.IndentOptions myIndentOptions;
private final int myRightMargin;
public BlockIndentOptions(@NotNull CodeStyleSettings settings, @NotNull CommonCodeStyleSettings.IndentOptions indentOptions, Block block) {
mySettings = settings;
myIndentOptions = indentOptions;
myRightMargin = calcRightMargin(block);
}
public CommonCodeStyleSettings.IndentOptions getIndentOptions() {
return myIndentOptions;
}
@NotNull
public CommonCodeStyleSettings.IndentOptions getIndentOptions(@NotNull AbstractBlockWrapper block) {
final Language language = block.getLanguage();
if (language == null) {
return myIndentOptions;
}
final CommonCodeStyleSettings commonSettings = mySettings.getCommonSettings(language);
if (commonSettings == null) {
return myIndentOptions;
}
final CommonCodeStyleSettings.IndentOptions result = commonSettings.getIndentOptions();
return result == null ? myIndentOptions : result;
}
public int getRightMargin() {
return myRightMargin;
}
private int calcRightMargin(Block rootBlock) {
Language language = null;
if (rootBlock instanceof ASTBlock) {
ASTNode node = ((ASTBlock)rootBlock).getNode();
if (node != null) {
PsiElement psiElement = node.getPsi();
if (psiElement.isValid()) {
PsiFile psiFile = psiElement.getContainingFile();
if (psiFile != null) {
language = psiFile.getViewProvider().getBaseLanguage();
}
}
}
}
return mySettings.getRightMargin(language);
}
}
@@ -0,0 +1,88 @@
/*
* Copyright 2000-2015 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.formatting.engine;
import com.intellij.formatting.LeafBlockWrapper;
import com.intellij.openapi.util.TextRange;
import gnu.trove.TIntObjectHashMap;
import org.jetbrains.annotations.Nullable;
public class BlockRangesMap {
private final LeafBlockWrapper myLastBlock;
private TIntObjectHashMap<LeafBlockWrapper> myTextRangeToWrapper;
public BlockRangesMap(LeafBlockWrapper first, LeafBlockWrapper last) {
myLastBlock = last;
myTextRangeToWrapper = buildTextRangeToInfoMap(first);
}
private static TIntObjectHashMap<LeafBlockWrapper> buildTextRangeToInfoMap(final LeafBlockWrapper first) {
final TIntObjectHashMap<LeafBlockWrapper> result = new TIntObjectHashMap<LeafBlockWrapper>();
LeafBlockWrapper current = first;
while (current != null) {
result.put(current.getStartOffset(), current);
current = current.getNextBlock();
}
return result;
}
public boolean containsLineFeeds(final TextRange dependency) {
LeafBlockWrapper child = myTextRangeToWrapper.get(dependency.getStartOffset());
if (child == null) return false;
if (child.containsLineFeeds()) return true;
final int endOffset = dependency.getEndOffset();
while (child.getEndOffset() < endOffset) {
child = child.getNextBlock();
if (child == null) return false;
if (child.getWhiteSpace().containsLineFeeds()) return true;
if (child.containsLineFeeds()) return true;
}
return false;
}
@Nullable
public LeafBlockWrapper getBlockAtOrAfter(final int startOffset) {
int current = startOffset;
LeafBlockWrapper result = null;
while (current < myLastBlock.getEndOffset()) {
final LeafBlockWrapper currentValue = myTextRangeToWrapper.get(current);
if (currentValue != null) {
result = currentValue;
break;
}
current++;
}
LeafBlockWrapper prevBlock = getPrevBlock(result);
if (prevBlock != null && prevBlock.contains(startOffset)) {
return prevBlock;
}
else {
return result;
}
}
@Nullable
private LeafBlockWrapper getPrevBlock(@Nullable final LeafBlockWrapper result) {
if (result != null) {
return result.getPreviousBlock();
}
else {
return myLastBlock;
}
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2000-2015 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.formatting.engine;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.TextChange;
import com.intellij.openapi.editor.impl.BulkChangesMerger;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CaretOffsetUpdater {
private final Map<Editor, Integer> myCaretOffsets = new HashMap<Editor, Integer>();
public CaretOffsetUpdater(@NotNull Document document) {
Editor[] editors = EditorFactory.getInstance().getEditors(document);
for (Editor editor : editors) {
myCaretOffsets.put(editor, editor.getCaretModel().getOffset());
}
}
public void update(@NotNull List<? extends TextChange> changes) {
BulkChangesMerger merger = BulkChangesMerger.INSTANCE;
for (Map.Entry<Editor, Integer> entry : myCaretOffsets.entrySet()) {
entry.setValue(merger.updateOffset(entry.getValue(), changes));
}
}
public void restoreCaretLocations() {
for (Map.Entry<Editor, Integer> entry : myCaretOffsets.entrySet()) {
entry.getKey().getCaretModel().moveToOffset(entry.getValue());
}
}
}
@@ -0,0 +1,109 @@
/*
* Copyright 2000-2015 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.formatting.engine;
import com.intellij.formatting.DependantSpacingImpl;
import com.intellij.formatting.SpacingImpl;
import com.intellij.formatting.WhiteSpace;
import com.intellij.openapi.util.TextRange;
import java.util.*;
/**
* Formatter provides a notion of {@link DependantSpacingImpl dependent spacing}, i.e. spacing that insist on line feed if target
* dependent region contains line feed.
* <p/>
* Example:
* <pre>
* int[] data = {1, 2, 3};
* </pre>
* We want to keep that in one line if possible but place curly braces on separate lines if the width is not enough:
* <pre>
* int[] data = { | &lt; right margin
* 1, 2, 3 |
* } |
* </pre>
* There is a possible case that particular block has dependent spacing property that targets region that lays beyond the
* current block. E.g. consider example above - <code>'1'</code> block has dependent spacing that targets the whole
* <code>'{1, 2, 3}'</code> block. So, it's not possible to answer whether line feed should be used during processing block
* <code>'1'</code>.
* <p/>
* We store such 'forward dependencies' at the current collection where the key is the range of the target 'dependent forward
* region' and value is dependent spacing object.
* <p/>
* Every time we detect that formatter changes 'has line feeds' status of such dependent region, we
* {@link DependantSpacingImpl#setDependentRegionLinefeedStatusChanged() mark} the dependent spacing as changed and schedule one more
* formatting iteration.
*/
public class DependentSpacingEngine {
private final BlockRangesMap myBlockRangesMap;
private SortedMap<TextRange, DependantSpacingImpl> myPreviousDependencies =
new TreeMap<TextRange, DependantSpacingImpl>(new Comparator<TextRange>() {
@Override
public int compare(final TextRange o1, final TextRange o2) {
int offsetsDelta = o1.getEndOffset() - o2.getEndOffset();
if (offsetsDelta == 0) {
offsetsDelta = o2.getStartOffset() - o1.getStartOffset(); // starting earlier is greater
}
return offsetsDelta;
}
});
public DependentSpacingEngine(BlockRangesMap helper) {
myBlockRangesMap = helper;
}
public boolean shouldReformatPreviouslyLocatedDependentSpacing(WhiteSpace space) {
final TextRange changed = space.getTextRange();
final SortedMap<TextRange, DependantSpacingImpl> sortedHeadMap = myPreviousDependencies.tailMap(changed);
for (final Map.Entry<TextRange, DependantSpacingImpl> entry : sortedHeadMap.entrySet()) {
final TextRange textRange = entry.getKey();
if (textRange.contains(changed)) {
final DependantSpacingImpl spacing = entry.getValue();
if (spacing.isDependentRegionLinefeedStatusChanged()) {
continue;
}
final boolean containedLineFeeds = spacing.getMinLineFeeds() > 0;
final boolean containsLineFeeds = myBlockRangesMap.containsLineFeeds(textRange);
if (containedLineFeeds != containsLineFeeds) {
spacing.setDependentRegionLinefeedStatusChanged();
return true;
}
}
}
return false;
}
public void registerUnresolvedDependentSpacingRanges(final SpacingImpl spaceProperty, List<TextRange> unprocessedRanges) {
final DependantSpacingImpl dependantSpaceProperty = (DependantSpacingImpl)spaceProperty;
if (dependantSpaceProperty.isDependentRegionLinefeedStatusChanged()) return;
for (TextRange range: unprocessedRanges) {
myPreviousDependencies.put(range, dependantSpaceProperty);
}
}
public void clear() {
myPreviousDependencies.clear();
}
}
@@ -0,0 +1,221 @@
/*
* Copyright 2000-2015 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.formatting.engine;
import com.intellij.formatting.*;
import com.intellij.openapi.editor.Document;
import com.intellij.util.containers.MultiMap;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
public class ExpandChildrenIndentState extends State {
private final Document myDocument;
private final WrapBlocksState myWrapState;
private IndentAdjuster myIndentAdjuster;
private MultiMap<ExpandableIndent, AbstractBlockWrapper> myExpandableIndents;
private LeafBlockWrapper myCurrentBlock;
private Iterator<ExpandableIndent> myIterator;
private MultiMap<Alignment, LeafBlockWrapper> myBlocksToRealign = new MultiMap<Alignment, LeafBlockWrapper>();
public ExpandChildrenIndentState(Document document, WrapBlocksState state) {
myDocument = document;
myWrapState = state;
}
@Override
public void prepare() {
myExpandableIndents = myWrapState.getExpandableIndent();
myIndentAdjuster = myWrapState.getIndentAdjuster();
myIterator = myExpandableIndents.keySet().iterator();
}
@Override
protected void doIteration() {
if (!myIterator.hasNext()) {
setDone(true);
return;
}
final ExpandableIndent indent = myIterator.next();
Collection<AbstractBlockWrapper> blocksToExpandIndent = myExpandableIndents.get(indent);
if (shouldExpand(blocksToExpandIndent)) {
for (AbstractBlockWrapper block : blocksToExpandIndent) {
indent.setEnforceIndent(true);
reindentNewLineChildren(block);
indent.setEnforceIndent(false);
}
}
restoreAlignments(myBlocksToRealign);
myBlocksToRealign.clear();
}
private void restoreAlignments(MultiMap<Alignment, LeafBlockWrapper> blocks) {
for (Alignment alignment : blocks.keySet()) {
AlignmentImpl alignmentImpl = (AlignmentImpl)alignment;
if (!alignmentImpl.isAllowBackwardShift()) continue;
Set<LeafBlockWrapper> toRealign = alignmentImpl.getOffsetResponsibleBlocks();
arrangeSpaces(toRealign);
LeafBlockWrapper rightMostBlock = getRightMostBlock(toRealign);
int maxSpacesBeforeBlock = rightMostBlock.getNumberOfSymbolsBeforeBlock().getTotalSpaces();
int rightMostBlockLine = myDocument.getLineNumber(rightMostBlock.getStartOffset());
for (LeafBlockWrapper block : toRealign) {
int currentBlockLine = myDocument.getLineNumber(block.getStartOffset());
if (currentBlockLine == rightMostBlockLine) continue;
int blockIndent = block.getNumberOfSymbolsBeforeBlock().getTotalSpaces();
int delta = maxSpacesBeforeBlock - blockIndent;
if (delta > 0) {
int newSpaces = block.getWhiteSpace().getTotalSpaces() + delta;
adjustSpacingToKeepAligned(block, newSpaces);
}
}
}
}
private void adjustSpacingToKeepAligned(LeafBlockWrapper block, int newSpaces) {
WhiteSpace space = block.getWhiteSpace();
SpacingImpl property = block.getSpaceProperty();
if (property == null) return;
space.arrangeSpaces(new SpacingImpl(newSpaces, newSpaces,
property.getMinLineFeeds(),
property.isReadOnly(),
property.isSafe(),
property.shouldKeepLineFeeds(),
property.getKeepBlankLines(),
property.shouldKeepFirstColumn(),
property.getPrefLineFeeds()));
}
private LeafBlockWrapper getRightMostBlock(Collection<LeafBlockWrapper> toRealign) {
int maxSpacesBeforeBlock = -1;
LeafBlockWrapper rightMostBlock = null;
for (LeafBlockWrapper block : toRealign) {
int spaces = block.getNumberOfSymbolsBeforeBlock().getTotalSpaces();
if (spaces > maxSpacesBeforeBlock) {
maxSpacesBeforeBlock = spaces;
rightMostBlock = block;
}
}
return rightMostBlock;
}
private void arrangeSpaces(Collection<LeafBlockWrapper> toRealign) {
for (LeafBlockWrapper block : toRealign) {
WhiteSpace whiteSpace = block.getWhiteSpace();
SpacingImpl spacing = block.getSpaceProperty();
whiteSpace.arrangeSpaces(spacing);
}
}
private boolean shouldExpand(Collection<AbstractBlockWrapper> blocksToExpandIndent) {
AbstractBlockWrapper last = null;
for (AbstractBlockWrapper block : blocksToExpandIndent) {
if (block.getWhiteSpace().containsLineFeeds()) {
return true;
}
last = block;
}
if (last != null) {
AbstractBlockWrapper next = getNextBlock(last);
if (next != null && next.getWhiteSpace().containsLineFeeds()) {
int nextNewLineBlockIndent = next.getNumberOfSymbolsBeforeBlock().getTotalSpaces();
if (nextNewLineBlockIndent >= finMinNewLineIndent(blocksToExpandIndent)) {
return true;
}
}
}
return false;
}
private int finMinNewLineIndent(@NotNull Collection<AbstractBlockWrapper> wrappers) {
int totalMinimum = Integer.MAX_VALUE;
for (AbstractBlockWrapper wrapper : wrappers) {
int minNewLineIndent = findMinNewLineIndent(wrapper);
if (minNewLineIndent < totalMinimum) {
totalMinimum = minNewLineIndent;
}
}
return totalMinimum;
}
private int findMinNewLineIndent(@NotNull AbstractBlockWrapper block) {
if (block instanceof LeafBlockWrapper && block.getWhiteSpace().containsLineFeeds()) {
return block.getNumberOfSymbolsBeforeBlock().getTotalSpaces();
}
else if (block instanceof CompositeBlockWrapper) {
List<AbstractBlockWrapper> children = ((CompositeBlockWrapper)block).getChildren();
int currentMin = Integer.MAX_VALUE;
for (AbstractBlockWrapper child : children) {
int childIndent = findMinNewLineIndent(child);
if (childIndent < currentMin) {
currentMin = childIndent;
}
}
return currentMin;
}
return Integer.MAX_VALUE;
}
private AbstractBlockWrapper getNextBlock(AbstractBlockWrapper block) {
List<AbstractBlockWrapper> children = block.getParent().getChildren();
int nextBlockIndex = children.indexOf(block) + 1;
if (nextBlockIndex < children.size()) {
return children.get(nextBlockIndex);
}
return null;
}
private void reindentNewLineChildren(final @NotNull AbstractBlockWrapper block) {
if (block instanceof LeafBlockWrapper) {
WhiteSpace space = block.getWhiteSpace();
if (space.containsLineFeeds()) {
myCurrentBlock = (LeafBlockWrapper)block;
myIndentAdjuster.adjustIndent(myCurrentBlock); //since aligned block starts new line, it should not touch any other block
storeAlignmentsAfterCurrentBlock();
}
}
else if (block instanceof CompositeBlockWrapper) {
List<AbstractBlockWrapper> children = ((CompositeBlockWrapper)block).getChildren();
for (AbstractBlockWrapper childBlock : children) {
reindentNewLineChildren(childBlock);
}
}
}
private void storeAlignmentsAfterCurrentBlock() {
LeafBlockWrapper current = myCurrentBlock.getNextBlock();
while (current != null && !current.getWhiteSpace().containsLineFeeds()) {
if (current.getAlignment() != null) {
myBlocksToRealign.putValue(current.getAlignment(), current);
}
current = current.getNextBlock();
}
}
}
@@ -13,7 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.formatting;
package com.intellij.formatting.engine;
import com.intellij.formatting.IndentImpl;
public class ExpandableIndent extends IndentImpl {
private boolean myEnforceIndent;
@@ -0,0 +1,65 @@
/*
* Copyright 2000-2015 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.formatting.engine;
import com.intellij.formatting.*;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import org.jetbrains.annotations.NotNull;
public class FormatProcessorUtils {
private static int calcShift(@NotNull final IndentInside oldIndent,
@NotNull final IndentInside newIndent,
@NotNull final CommonCodeStyleSettings.IndentOptions options)
{
if (oldIndent.equals(newIndent)) return 0;
return newIndent.getSpacesCount(options) - oldIndent.getSpacesCount(options);
}
public static int replaceWhiteSpace(final FormattingModel model,
@NotNull final LeafBlockWrapper block,
int shift,
final CharSequence _newWhiteSpace,
final CommonCodeStyleSettings.IndentOptions options
) {
final WhiteSpace whiteSpace = block.getWhiteSpace();
final TextRange textRange = whiteSpace.getTextRange();
final TextRange wsRange = textRange.shiftRight(shift);
final String newWhiteSpace = _newWhiteSpace.toString();
TextRange newWhiteSpaceRange = model instanceof FormattingModelEx
? ((FormattingModelEx) model).replaceWhiteSpace(wsRange, block.getNode(), newWhiteSpace)
: model.replaceWhiteSpace(wsRange, newWhiteSpace);
shift += newWhiteSpaceRange.getLength() - textRange.getLength();
if (block.isLeaf() && whiteSpace.containsLineFeeds() && block.containsLineFeeds()) {
final TextRange currentBlockRange = block.getTextRange().shiftRight(shift);
IndentInside oldBlockIndent = whiteSpace.getInitialLastLineIndent();
IndentInside whiteSpaceIndent = IndentInside.createIndentOn(IndentInside.getLastLine(newWhiteSpace));
final int shiftInside = calcShift(oldBlockIndent, whiteSpaceIndent, options);
if (shiftInside != 0 || !oldBlockIndent.equals(whiteSpaceIndent)) {
final TextRange newBlockRange = model.shiftIndentInsideRange(block.getNode(), currentBlockRange, shiftInside);
shift += newBlockRange.getLength() - block.getLength();
}
}
return shift;
}
}
@@ -0,0 +1,143 @@
/*
* Copyright 2000-2015 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.formatting.engine;
import com.intellij.formatting.*;
import com.intellij.formatting.FormatProcessor;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import org.jetbrains.annotations.Nullable;
public class IndentAdjuster {
private final AlignmentHelper myAlignmentHelper;
private final BlockIndentOptions myBlockIndentOptions;
public IndentAdjuster(BlockIndentOptions blockIndentOptions, AlignmentHelper alignmentHelper) {
myAlignmentHelper = alignmentHelper;
myBlockIndentOptions = blockIndentOptions;
}
/**
* Sometimes to align block we adjust whitespace of other block before.
* In such a case, we rollback to that block restarting formatting from there.
*
* @return new current block if we need to rollback, null otherwise
*/
public LeafBlockWrapper adjustIndent(LeafBlockWrapper block) {
AlignmentImpl alignment = CoreFormatterUtil.getAlignment(block);
WhiteSpace whiteSpace = block.getWhiteSpace();
if (alignment == null || myAlignmentHelper.shouldSkip(alignment)) {
if (whiteSpace.containsLineFeeds()) {
adjustSpacingByIndentOffset(block);
}
else {
whiteSpace.arrangeSpaces(block.getSpaceProperty());
}
return null;
}
return myAlignmentHelper.applyAlignment(alignment, block);
}
private void adjustSpacingByIndentOffset(LeafBlockWrapper block) {
CommonCodeStyleSettings.IndentOptions options = myBlockIndentOptions.getIndentOptions(block);
IndentData offset = block.calculateOffset(options);
block.getWhiteSpace().setSpaces(offset.getSpaces(), offset.getIndentSpaces());
}
public void adjustLineIndent(LeafBlockWrapper myCurrentBlock) {
IndentData alignOffset = getAlignOffset(myCurrentBlock);
if (alignOffset == null) {
adjustSpacingByIndentOffset(myCurrentBlock);
}
else {
myCurrentBlock.getWhiteSpace().setSpaces(alignOffset.getSpaces(), alignOffset.getIndentSpaces());
}
}
@Nullable
private static IndentData getAlignOffset(LeafBlockWrapper myCurrentBlock) {
AbstractBlockWrapper current = myCurrentBlock;
while (true) {
final AlignmentImpl alignment = current.getAlignment();
LeafBlockWrapper offsetResponsibleBlock;
if (alignment != null && (offsetResponsibleBlock = alignment.getOffsetRespBlockBefore(myCurrentBlock)) != null) {
final WhiteSpace whiteSpace = offsetResponsibleBlock.getWhiteSpace();
if (whiteSpace.containsLineFeeds()) {
return new IndentData(whiteSpace.getIndentSpaces(), whiteSpace.getSpaces());
}
else {
final int offsetBeforeBlock = CoreFormatterUtil.getStartColumn(offsetResponsibleBlock);
final AbstractBlockWrapper indentedParentBlock = CoreFormatterUtil.getIndentedParentBlock(myCurrentBlock);
if (indentedParentBlock == null) {
return new IndentData(0, offsetBeforeBlock);
}
else {
final int parentIndent = indentedParentBlock.getWhiteSpace().getIndentOffset();
if (parentIndent > offsetBeforeBlock) {
return new IndentData(0, offsetBeforeBlock);
}
else {
return new IndentData(parentIndent, offsetBeforeBlock - parentIndent);
}
}
}
}
else {
current = current.getParent();
if (current == null || current.getStartOffset() != myCurrentBlock.getStartOffset()) return null;
}
}
}
public IndentInfo adjustLineIndent(LeafBlockWrapper currentBlock, FormatProcessor.ChildAttributesInfo info) {
AbstractBlockWrapper parent = info.parent;
ChildAttributes childAttributes = info.attributes;
int index = info.index;
int alignOffset = getAlignOffsetBefore(childAttributes.getAlignment(), null);
if (alignOffset == -1) {
return parent.calculateChildOffset(myBlockIndentOptions.getIndentOptions(parent), childAttributes, index).createIndentInfo();
}
else {
AbstractBlockWrapper indentedParentBlock = CoreFormatterUtil.getIndentedParentBlock(currentBlock);
if (indentedParentBlock == null) {
return new IndentInfo(0, 0, alignOffset);
}
else {
int indentOffset = indentedParentBlock.getWhiteSpace().getIndentOffset();
if (indentOffset > alignOffset) {
return new IndentInfo(0, 0, alignOffset);
}
else {
return new IndentInfo(0, indentOffset, alignOffset - indentOffset);
}
}
}
}
private static int getAlignOffsetBefore(@Nullable final Alignment alignment, @Nullable final LeafBlockWrapper blockAfter) {
if (alignment == null) return -1;
final LeafBlockWrapper alignRespBlock = ((AlignmentImpl)alignment).getOffsetRespBlockBefore(blockAfter);
if (alignRespBlock != null) {
return CoreFormatterUtil.getStartColumn(alignRespBlock);
}
else {
return -1;
}
}
}
@@ -0,0 +1,50 @@
/*
* Copyright 2000-2015 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.formatting.engine;
public abstract class State {
private boolean myDone;
private Runnable myOnDoneAction;
public void iteration() {
if (!isDone()) {
doIteration();
}
}
public boolean isDone() {
return myDone;
}
protected void setDone(boolean done) {
if (!myDone && done && myOnDoneAction != null) {
myOnDoneAction.run();
}
myDone = done;
}
public void stop() {}
protected abstract void doIteration();
public void prepare() {}
public void setOnDone(Runnable onDoneAction) {
myOnDoneAction = onDoneAction;
}
}
@@ -0,0 +1,57 @@
/*
* Copyright 2000-2015 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.formatting.engine;
import com.intellij.util.containers.ContainerUtil;
import java.util.List;
public class StateProcessor {
private final List<State> myStates = ContainerUtil.newArrayList();
private State myCurrentState;
public StateProcessor(State initial) {
myCurrentState = initial;
}
public void setNextState(State state) {
myStates.add(state);
}
public boolean isDone() {
return myStates.isEmpty() && myCurrentState.isDone();
}
public void iteration() {
if (!myCurrentState.isDone()) {
myCurrentState.iteration();
}
shiftStateIfNecessary();
}
private void shiftStateIfNecessary() {
if (myCurrentState.isDone() && !myStates.isEmpty()) {
myCurrentState = myStates.get(0);
myStates.remove(0);
myCurrentState.prepare();
}
}
public void stop() {
myCurrentState.stop();
}
}
@@ -0,0 +1,136 @@
/*
* Copyright 2000-2015 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.formatting.engine;
import com.intellij.formatting.*;
import com.intellij.openapi.editor.Document;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.util.containers.MultiMap;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.Set;
public class WrapBlocksState extends State {
private final InitialInfoBuilder myWrapper;
private final BlockIndentOptions myBlockIndentOptions;
private WhiteSpace myLastWhiteSpace;
private BlockRangesMap myBlockRangesMap;
private DependentSpacingEngine myDependentSpacingEngine;
private AlignmentHelper myAlignmentHelper;
private IndentAdjuster myIndentAdjuster;
private WrapProcessor myWrapProcessor;
public WrapBlocksState(@NotNull InitialInfoBuilder initialInfoBuilder, BlockIndentOptions blockIndentOptions) {
myWrapper = initialInfoBuilder;
myBlockIndentOptions = blockIndentOptions;
}
@Override
public void doIteration() {
if (isDone()) {
return;
}
setDone(myWrapper.iteration());
}
public Map<AbstractBlockWrapper, Block> getBlockToInfoMap() {
return myWrapper.getBlockToInfoMap();
}
public LeafBlockWrapper getFirstBlock() {
assertDone();
return myWrapper.getFirstTokenBlock();
}
public LeafBlockWrapper getLastBlock() {
assertDone();
return myWrapper.getLastTokenBlock();
}
public WhiteSpace getLastWhiteSpace() {
assertDone();
if (myLastWhiteSpace == null) {
int lastBlockOffset = getLastBlock().getEndOffset();
myLastWhiteSpace = new WhiteSpace(lastBlockOffset, false);
FormattingDocumentModel model = myWrapper.getFormattingDocumentModel();
CommonCodeStyleSettings.IndentOptions options = myBlockIndentOptions.getIndentOptions();
myLastWhiteSpace.append(Math.max(lastBlockOffset, myWrapper.getEndOffset()), model, options);
}
return myLastWhiteSpace;
}
public BlockRangesMap getBlockRangesMap() {
assertDone();
if (myBlockRangesMap == null) {
myBlockRangesMap = new BlockRangesMap(getFirstBlock(), getLastBlock());
}
return myBlockRangesMap;
}
public DependentSpacingEngine getDependentSpacingEngine() {
assertDone();
if (myDependentSpacingEngine == null) {
myDependentSpacingEngine = new DependentSpacingEngine(getBlockRangesMap());
}
return myDependentSpacingEngine;
}
public Set<Alignment> getAlignmentsInsideRangesToModify() {
assertDone();
return myWrapper.getAlignmentsInsideRangeToModify();
}
public AlignmentHelper getAlignmentHelper() {
assertDone();
if (myAlignmentHelper == null) {
Document document = myWrapper.getFormattingDocumentModel().getDocument();
myAlignmentHelper = new AlignmentHelper(document, myWrapper.getBlocksToAlign(), myBlockIndentOptions);
}
return myAlignmentHelper;
}
public IndentAdjuster getIndentAdjuster() {
assertDone();
if (myIndentAdjuster == null) {
myIndentAdjuster = new IndentAdjuster(myBlockIndentOptions, getAlignmentHelper());
}
return myIndentAdjuster;
}
public MultiMap<ExpandableIndent, AbstractBlockWrapper> getExpandableIndent() {
assertDone();
return myWrapper.getExpandableIndentsBlocks();
}
public WrapProcessor getWrapProcessor() {
assertDone();
if (myWrapProcessor == null) {
int rightMargin = myBlockIndentOptions.getRightMargin();
myWrapProcessor = new WrapProcessor(myBlockRangesMap, getIndentAdjuster(), rightMargin);
}
return myWrapProcessor;
}
public BlockIndentOptions getBlockIndentOptions() {
return myBlockIndentOptions;
}
private void assertDone() {
if (!isDone()) throw new IllegalStateException();
}
}
@@ -0,0 +1,213 @@
/*
* Copyright 2000-2015 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.formatting.engine;
import com.intellij.formatting.*;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
public class WrapProcessor {
private LeafBlockWrapper myFirstWrappedBlockOnLine = null;
private BlockRangesMap myBlockRangesMap;
private LeafBlockWrapper myWrapCandidate = null;
private IndentAdjuster myIndentAdjuster;
private int myRightMargin;
public WrapProcessor(BlockRangesMap blockHelper, IndentAdjuster indentAdjuster, int rightMargin) {
myIndentAdjuster = indentAdjuster;
myBlockRangesMap = blockHelper;
myRightMargin = rightMargin;
}
private boolean isSuitableInTheCurrentPosition(final WrapImpl wrap, LeafBlockWrapper currentBlock) {
if (wrap.getWrapOffset() < currentBlock.getStartOffset()) {
return true;
}
if (wrap.isWrapFirstElement()) {
return true;
}
if (wrap.getType() == WrapImpl.Type.WRAP_AS_NEEDED) {
return positionAfterWrappingIsSuitable(currentBlock);
}
return wrap.getType() == WrapImpl.Type.CHOP_IF_NEEDED && lineOver(currentBlock) && positionAfterWrappingIsSuitable(currentBlock);
}
/**
* @return <code>true</code> if {@link #myCurrentBlock currently processed wrapped block} doesn't contain line feeds and
* exceeds right margin; <code>false</code> otherwise
*/
private boolean lineOver(LeafBlockWrapper currentBlock) {
return !currentBlock.containsLineFeeds() &&
CoreFormatterUtil.getStartColumn(currentBlock) + currentBlock.getLength() > myRightMargin;
}
/**
* Ensures that offset of the {@link #myCurrentBlock currently processed block} is not increased if we make a wrap on it.
*
* @return <code>true</code> if it's ok to wrap at the currently processed block; <code>false</code> otherwise
*/
private boolean positionAfterWrappingIsSuitable(LeafBlockWrapper currentBlock) {
final WhiteSpace whiteSpace = currentBlock.getWhiteSpace();
if (whiteSpace.containsLineFeeds()) return true;
final int spaces = whiteSpace.getSpaces();
int indentSpaces = whiteSpace.getIndentSpaces();
try {
final int startColumnNow = CoreFormatterUtil.getStartColumn(currentBlock);
whiteSpace.ensureLineFeed();
myIndentAdjuster.adjustLineIndent(currentBlock);
final int startColumnAfterWrap = CoreFormatterUtil.getStartColumn(currentBlock);
return startColumnNow > startColumnAfterWrap;
}
finally {
whiteSpace.removeLineFeeds(currentBlock.getSpaceProperty(), myBlockRangesMap);
whiteSpace.setSpaces(spaces, indentSpaces);
}
}
@Nullable
private WrapImpl getWrapToBeUsed(final ArrayList<WrapImpl> wraps, LeafBlockWrapper currentBlock) {
if (wraps.isEmpty()) {
return null;
}
if (myWrapCandidate == currentBlock) return wraps.get(0);
for (final WrapImpl wrap : wraps) {
if (!isSuitableInTheCurrentPosition(wrap, currentBlock)) continue;
if (wrap.isActive()) return wrap;
final WrapImpl.Type type = wrap.getType();
if (type == WrapImpl.Type.WRAP_ALWAYS) return wrap;
if (type == WrapImpl.Type.WRAP_AS_NEEDED || type == WrapImpl.Type.CHOP_IF_NEEDED) {
if (lineOver(currentBlock)) {
return wrap;
}
}
}
return null;
}
private boolean isCandidateToBeWrapped(final WrapImpl wrap, LeafBlockWrapper currentBlock) {
return isSuitableInTheCurrentPosition(wrap, currentBlock) &&
(wrap.getType() == WrapImpl.Type.WRAP_AS_NEEDED || wrap.getType() == WrapImpl.Type.CHOP_IF_NEEDED) &&
!currentBlock.getWhiteSpace().isReadOnly();
}
/**
* Allows to answer if wrap of the {@link #myWrapCandidate} object (if any) may be replaced by the given wrap.
*
* @param wrap wrap candidate to check
* @return <code>true</code> if wrap of the {@link #myWrapCandidate} object (if any) may be replaced by the given wrap;
* <code>false</code> otherwise
*/
private boolean canReplaceWrapCandidate(WrapImpl wrap, LeafBlockWrapper currentBlock) {
if (myWrapCandidate == null) return true;
WrapImpl.Type type = wrap.getType();
if (wrap.isActive() && (type == WrapImpl.Type.CHOP_IF_NEEDED || type == WrapImpl.Type.WRAP_ALWAYS)) return true;
final WrapImpl currentWrap = myWrapCandidate.getWrap();
return wrap == currentWrap || !wrap.isChildOf(currentWrap, currentBlock);
}
public LeafBlockWrapper processWrap(LeafBlockWrapper currentBlock) {
final SpacingImpl spacing = currentBlock.getSpaceProperty();
final WhiteSpace whiteSpace = currentBlock.getWhiteSpace();
final boolean wrapWasPresent = whiteSpace.containsLineFeeds();
if (wrapWasPresent) {
myFirstWrappedBlockOnLine = null;
if (!whiteSpace.containsLineFeedsInitially()) {
whiteSpace.removeLineFeeds(spacing, myBlockRangesMap);
}
}
final boolean wrapIsPresent = whiteSpace.containsLineFeeds();
final ArrayList<WrapImpl> wraps = currentBlock.getWraps();
for (WrapImpl wrap : wraps) {
wrap.setWrapOffset(currentBlock.getStartOffset());
}
final WrapImpl wrap = getWrapToBeUsed(wraps, currentBlock);
if (wrap != null || wrapIsPresent) {
if (!wrapIsPresent && !canReplaceWrapCandidate(wrap, currentBlock)) {
return myWrapCandidate;
}
if (wrap != null && wrap.getChopStartBlock() != null) {
// getWrapToBeUsed() returns the block only if it actually exceeds the right margin. In this case, we need to go back to the
// first block that has the CHOP_IF_NEEDED wrap type and start wrapping from there.
LeafBlockWrapper newCurrentBlock = wrap.getChopStartBlock();
wrap.setActive();
return newCurrentBlock;
}
if (wrap != null && isChopNeeded(wrap, currentBlock)) {
wrap.setActive();
}
if (!wrapIsPresent) {
whiteSpace.ensureLineFeed();
if (!wrapWasPresent) {
if (myFirstWrappedBlockOnLine != null && wrap.isChildOf(myFirstWrappedBlockOnLine.getWrap(), currentBlock)) {
wrap.ignoreParentWrap(myFirstWrappedBlockOnLine.getWrap(), currentBlock);
return myFirstWrappedBlockOnLine;
}
else {
myFirstWrappedBlockOnLine = currentBlock;
}
}
}
myWrapCandidate = null;
}
else {
for (final WrapImpl wrap1 : wraps) {
if (isCandidateToBeWrapped(wrap1, currentBlock) && canReplaceWrapCandidate(wrap1, currentBlock)) {
myWrapCandidate = currentBlock;
}
if (isChopNeeded(wrap1, currentBlock)) {
wrap1.saveChopBlock(currentBlock);
}
}
}
if (!whiteSpace.containsLineFeeds() && myWrapCandidate != null && !whiteSpace.isReadOnly() && lineOver(currentBlock)) {
return myWrapCandidate;
}
return null;
}
private boolean isChopNeeded(final WrapImpl wrap, LeafBlockWrapper currentBlock) {
return wrap != null && wrap.getType() == WrapImpl.Type.CHOP_IF_NEEDED && isSuitableInTheCurrentPosition(wrap, currentBlock);
}
public void reset() {
myWrapCandidate = null;
}
public void onCurrentLineChanged() {
myWrapCandidate = null;
}
}