| /* |
| * 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.formatting; |
| |
| import com.intellij.lang.ASTNode; |
| import com.intellij.openapi.application.Application; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.fileTypes.FileType; |
| import com.intellij.openapi.progress.ProgressManager; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.Computable; |
| import com.intellij.openapi.util.Couple; |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.psi.PsiDocumentManager; |
| import com.intellij.psi.PsiElement; |
| import com.intellij.psi.PsiFile; |
| import com.intellij.psi.codeStyle.CodeStyleSettings; |
| import com.intellij.psi.codeStyle.CommonCodeStyleSettings; |
| import com.intellij.psi.formatter.FormatterUtil; |
| import com.intellij.psi.formatter.FormattingDocumentModelImpl; |
| import com.intellij.psi.formatter.PsiBasedFormattingModel; |
| import com.intellij.util.IncorrectOperationException; |
| import com.intellij.util.SequentialTask; |
| import com.intellij.util.text.CharArrayUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| public class FormatterImpl extends FormatterEx |
| implements IndentFactory, |
| WrapFactory, |
| AlignmentFactory, |
| SpacingFactory, |
| FormattingModelFactory { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.formatting.FormatterImpl"); |
| |
| private final AtomicReference<FormattingProgressTask> myProgressTask = new AtomicReference<FormattingProgressTask>(); |
| |
| private final AtomicInteger myIsDisabledCount = new AtomicInteger(); |
| private final IndentImpl NONE_INDENT = new IndentImpl(Indent.Type.NONE, false, false); |
| private final IndentImpl myAbsoluteNoneIndent = new IndentImpl(Indent.Type.NONE, true, false); |
| private final IndentImpl myLabelIndent = new IndentImpl(Indent.Type.LABEL, false, false); |
| private final IndentImpl myContinuationIndentRelativeToDirectParent = new IndentImpl(Indent.Type.CONTINUATION, false, true); |
| private final IndentImpl myContinuationIndentNotRelativeToDirectParent = new IndentImpl(Indent.Type.CONTINUATION, false, false); |
| private final IndentImpl myContinuationWithoutFirstIndentRelativeToDirectParent |
| = new IndentImpl(Indent.Type.CONTINUATION_WITHOUT_FIRST, false, true); |
| private final IndentImpl myContinuationWithoutFirstIndentNotRelativeToDirectParent |
| = new IndentImpl(Indent.Type.CONTINUATION_WITHOUT_FIRST, false, false); |
| private final IndentImpl myAbsoluteLabelIndent = new IndentImpl(Indent.Type.LABEL, true, false); |
| private final IndentImpl myNormalIndentRelativeToDirectParent = new IndentImpl(Indent.Type.NORMAL, false, true); |
| private final IndentImpl myNormalIndentNotRelativeToDirectParent = new IndentImpl(Indent.Type.NORMAL, false, false); |
| private final SpacingImpl myReadOnlySpacing = new SpacingImpl(0, 0, 0, true, false, true, 0, false, 0); |
| |
| public FormatterImpl() { |
| Indent.setFactory(this); |
| Wrap.setFactory(this); |
| Alignment.setFactory(this); |
| Spacing.setFactory(this); |
| FormattingModelProvider.setFactory(this); |
| } |
| |
| @Override |
| public Alignment createAlignment(boolean applyToNonFirstBlocksOnLine, @NotNull Alignment.Anchor anchor) { |
| return new AlignmentImpl(applyToNonFirstBlocksOnLine, anchor); |
| } |
| |
| @Override |
| public Alignment createChildAlignment(final Alignment base) { |
| AlignmentImpl result = new AlignmentImpl(); |
| result.setParent(base); |
| return result; |
| } |
| |
| @Override |
| public Indent getNormalIndent(boolean relative) { |
| return relative ? myNormalIndentRelativeToDirectParent : myNormalIndentNotRelativeToDirectParent; |
| } |
| |
| @Override |
| public Indent getNoneIndent() { |
| return NONE_INDENT; |
| } |
| |
| @Override |
| public void setProgressTask(@NotNull FormattingProgressTask progressIndicator) { |
| if (!FormatterUtil.isFormatterCalledExplicitly()) { |
| return; |
| } |
| myProgressTask.set(progressIndicator); |
| } |
| |
| @Override |
| public int getSpacingForBlockAtOffset(FormattingModel model, int offset) { |
| Couple<Block> blockWithParent = getBlockAtOffset(null, model.getRootBlock(), offset); |
| if (blockWithParent != null) { |
| Block parentBlock = blockWithParent.first; |
| Block targetBlock = blockWithParent.second; |
| if (parentBlock != null && targetBlock != null) { |
| Block prevBlock = findPreviousSibling(parentBlock, targetBlock); |
| if (prevBlock != null) { |
| SpacingImpl spacing = (SpacingImpl)parentBlock.getSpacing(prevBlock, targetBlock); |
| if (spacing != null) { |
| int minSpaces = spacing.getMinSpaces(); |
| if (minSpaces > 0) { |
| return minSpaces; |
| } |
| } |
| } |
| } |
| } |
| return 0; |
| } |
| |
| @Nullable |
| private static Couple<Block> getBlockAtOffset(@Nullable Block parent, @NotNull Block block, int offset) { |
| TextRange textRange = block.getTextRange(); |
| int startOffset = textRange.getStartOffset(); |
| int endOffset = textRange.getEndOffset(); |
| if (startOffset == offset) { |
| return Couple.of(parent, block); |
| } |
| if (startOffset > offset || endOffset < offset || block.isLeaf()) { |
| return null; |
| } |
| for (Block subBlock : block.getSubBlocks()) { |
| Couple<Block> result = getBlockAtOffset(block, subBlock, offset); |
| if (result != null) { |
| return result; |
| } |
| } |
| return null; |
| } |
| |
| @Nullable |
| private static Block findPreviousSibling(@NotNull Block parent, Block block) { |
| Block result = null; |
| for (Block subBlock : parent.getSubBlocks()) { |
| if (subBlock == block) { |
| return result; |
| } |
| result = subBlock; |
| } |
| return null; |
| } |
| |
| @Override |
| public void format(final FormattingModel model, final CodeStyleSettings settings, |
| final CommonCodeStyleSettings.IndentOptions indentOptions, |
| final CommonCodeStyleSettings.IndentOptions javaIndentOptions, |
| final FormatTextRanges affectedRanges) throws IncorrectOperationException |
| { |
| try { |
| validateModel(model); |
| SequentialTask task = new MyFormattingTask() { |
| @NotNull |
| @Override |
| protected FormatProcessor buildProcessor() { |
| FormatProcessor processor = new FormatProcessor( |
| model.getDocumentModel(), model.getRootBlock(), settings, indentOptions, affectedRanges, FormattingProgressCallback.EMPTY |
| ); |
| processor.setJavaIndentOptions(javaIndentOptions); |
| |
| processor.format(model); |
| return processor; |
| } |
| }; |
| execute(task); |
| } |
| catch (FormattingModelInconsistencyException e) { |
| LOG.error(e); |
| } |
| } |
| |
| @Override |
| public Wrap createWrap(WrapType type, boolean wrapFirstElement) { |
| return new WrapImpl(type, wrapFirstElement); |
| } |
| |
| @Override |
| public Wrap createChildWrap(final Wrap parentWrap, final WrapType wrapType, final boolean wrapFirstElement) { |
| final WrapImpl result = new WrapImpl(wrapType, wrapFirstElement); |
| result.registerParent((WrapImpl)parentWrap); |
| return result; |
| } |
| |
| @Override |
| @NotNull |
| public Spacing createSpacing(int minOffset, |
| int maxOffset, |
| int minLineFeeds, |
| final boolean keepLineBreaks, |
| final int keepBlankLines) { |
| return getSpacingImpl(minOffset, maxOffset, minLineFeeds, false, false, keepLineBreaks, keepBlankLines,false, 0); |
| } |
| |
| @Override |
| @NotNull |
| public Spacing getReadOnlySpacing() { |
| return myReadOnlySpacing; |
| } |
| |
| @NotNull |
| @Override |
| public Spacing createDependentLFSpacing(int minSpaces, |
| int maxSpaces, |
| @NotNull TextRange dependencyRange, |
| boolean keepLineBreaks, |
| int keepBlankLines, |
| @NotNull DependentSpacingRule rule) |
| { |
| return new DependantSpacingImpl(minSpaces, maxSpaces, dependencyRange, keepLineBreaks, keepBlankLines, rule); |
| } |
| |
| @NotNull |
| private FormattingProgressCallback getProgressCallback() { |
| FormattingProgressCallback result = myProgressTask.get(); |
| return result == null ? FormattingProgressCallback.EMPTY : result; |
| } |
| |
| @Override |
| public void format(final FormattingModel model, |
| final CodeStyleSettings settings, |
| final CommonCodeStyleSettings.IndentOptions indentOptions, |
| final FormatTextRanges affectedRanges) throws IncorrectOperationException { |
| try { |
| validateModel(model); |
| SequentialTask task = new MyFormattingTask() { |
| @NotNull |
| @Override |
| protected FormatProcessor buildProcessor() { |
| FormatProcessor processor = new FormatProcessor( |
| model.getDocumentModel(), model.getRootBlock(), settings, indentOptions, affectedRanges, getProgressCallback() |
| ); |
| processor.format(model, true); |
| return processor; |
| } |
| }; |
| execute(task); |
| } |
| catch (FormattingModelInconsistencyException e) { |
| LOG.error(e); |
| } |
| } |
| |
| public void formatWithoutModifications(final FormattingDocumentModel model, |
| final Block rootBlock, |
| final CodeStyleSettings settings, |
| final CommonCodeStyleSettings.IndentOptions indentOptions, |
| final TextRange affectedRange) throws IncorrectOperationException |
| { |
| SequentialTask task = new MyFormattingTask() { |
| @NotNull |
| @Override |
| protected FormatProcessor buildProcessor() { |
| FormatProcessor result = new FormatProcessor( |
| model, rootBlock, settings, indentOptions, new FormatTextRanges(affectedRange, true), FormattingProgressCallback.EMPTY |
| ); |
| result.formatWithoutRealModifications(); |
| return result; |
| } |
| }; |
| 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(); |
| FormattingProgressTask progressTask = myProgressTask.getAndSet(null); |
| if (progressTask == null || !application.isDispatchThread() || application.isUnitTestMode()) { |
| try { |
| task.prepare(); |
| while (!task.isDone()) { |
| task.iteration(); |
| } |
| } |
| finally { |
| enableFormatting(); |
| } |
| } |
| else { |
| progressTask.setTask(task); |
| Runnable callback = new Runnable() { |
| @Override |
| public void run() { |
| enableFormatting(); |
| } |
| }; |
| for (FormattingProgressCallback.EventType eventType : FormattingProgressCallback.EventType.values()) { |
| progressTask.addCallback(eventType, callback); |
| } |
| ProgressManager.getInstance().run(progressTask); |
| } |
| } |
| |
| @Override |
| public IndentInfo getWhiteSpaceBefore(final FormattingDocumentModel model, |
| final Block block, |
| final CodeStyleSettings settings, |
| final CommonCodeStyleSettings.IndentOptions indentOptions, |
| final TextRange affectedRange, final boolean mayChangeLineFeeds) |
| { |
| disableFormatting(); |
| try { |
| final FormatProcessor processor = buildProcessorAndWrapBlocks( |
| model, block, settings, indentOptions, new FormatTextRanges(affectedRange, true) |
| ); |
| final LeafBlockWrapper blockBefore = processor.getBlockAfter(affectedRange.getStartOffset()); |
| LOG.assertTrue(blockBefore != null); |
| WhiteSpace whiteSpace = blockBefore.getWhiteSpace(); |
| LOG.assertTrue(whiteSpace != null); |
| if (!mayChangeLineFeeds) { |
| whiteSpace.setLineFeedsAreReadOnly(); |
| } |
| processor.setAllWhiteSpacesAreReadOnly(); |
| whiteSpace.setReadOnly(false); |
| processor.formatWithoutRealModifications(); |
| return new IndentInfo(whiteSpace.getLineFeeds(), whiteSpace.getIndentOffset(), whiteSpace.getSpaces()); |
| } |
| finally { |
| enableFormatting(); |
| } |
| } |
| |
| @Override |
| public void adjustLineIndentsForRange(final FormattingModel model, |
| final CodeStyleSettings settings, |
| final CommonCodeStyleSettings.IndentOptions indentOptions, |
| final TextRange rangeToAdjust) { |
| disableFormatting(); |
| try { |
| validateModel(model); |
| final FormattingDocumentModel documentModel = model.getDocumentModel(); |
| final Block block = model.getRootBlock(); |
| final FormatProcessor processor = buildProcessorAndWrapBlocks( |
| documentModel, block, settings, indentOptions, new FormatTextRanges(rangeToAdjust, true) |
| ); |
| LeafBlockWrapper tokenBlock = processor.getFirstTokenBlock(); |
| while (tokenBlock != null) { |
| final WhiteSpace whiteSpace = tokenBlock.getWhiteSpace(); |
| whiteSpace.setLineFeedsAreReadOnly(true); |
| if (!whiteSpace.containsLineFeeds()) { |
| whiteSpace.setIsReadOnly(true); |
| } |
| tokenBlock = tokenBlock.getNextBlock(); |
| } |
| processor.formatWithoutRealModifications(); |
| processor.performModifications(model); |
| } |
| catch (FormattingModelInconsistencyException e) { |
| LOG.error(e); |
| } |
| finally { |
| enableFormatting(); |
| } |
| } |
| |
| @Override |
| public void formatAroundRange(final FormattingModel model, |
| final CodeStyleSettings settings, |
| final TextRange textRange, |
| final FileType fileType) { |
| disableFormatting(); |
| try { |
| validateModel(model); |
| final FormattingDocumentModel documentModel = model.getDocumentModel(); |
| final Block block = model.getRootBlock(); |
| final FormatProcessor processor = buildProcessorAndWrapBlocks( |
| documentModel, block, settings, settings.getIndentOptions(fileType), null |
| ); |
| LeafBlockWrapper tokenBlock = processor.getFirstTokenBlock(); |
| while (tokenBlock != null) { |
| final WhiteSpace whiteSpace = tokenBlock.getWhiteSpace(); |
| |
| if (whiteSpace.getEndOffset() < textRange.getStartOffset() || whiteSpace.getEndOffset() > textRange.getEndOffset() + 1) { |
| whiteSpace.setIsReadOnly(true); |
| } else if (whiteSpace.getStartOffset() > textRange.getStartOffset() && |
| whiteSpace.getEndOffset() < textRange.getEndOffset()) |
| { |
| if (whiteSpace.containsLineFeeds()) { |
| whiteSpace.setLineFeedsAreReadOnly(true); |
| } else { |
| whiteSpace.setIsReadOnly(true); |
| } |
| } |
| |
| tokenBlock = tokenBlock.getNextBlock(); |
| } |
| processor.formatWithoutRealModifications(); |
| processor.performModifications(model); |
| } |
| catch (FormattingModelInconsistencyException e) { |
| LOG.error(e); |
| } |
| finally{ |
| enableFormatting(); |
| } |
| } |
| |
| @Override |
| public int adjustLineIndent(final FormattingModel model, |
| final CodeStyleSettings settings, |
| final CommonCodeStyleSettings.IndentOptions indentOptions, |
| final int offset, |
| final TextRange affectedRange) throws IncorrectOperationException { |
| disableFormatting(); |
| try { |
| validateModel(model); |
| if (model instanceof PsiBasedFormattingModel) { |
| ((PsiBasedFormattingModel)model).canModifyAllWhiteSpaces(); |
| } |
| final FormattingDocumentModel documentModel = model.getDocumentModel(); |
| final Block block = model.getRootBlock(); |
| final FormatProcessor processor = buildProcessorAndWrapBlocks( |
| documentModel, block, settings, indentOptions, new FormatTextRanges(affectedRange, true), offset |
| ); |
| |
| final LeafBlockWrapper blockAfterOffset = processor.getBlockAfter(offset); |
| |
| if (blockAfterOffset != null && blockAfterOffset.contains(offset)) { |
| return offset; |
| } |
| |
| WhiteSpace whiteSpace = blockAfterOffset != null ? blockAfterOffset.getWhiteSpace() : processor.getLastWhiteSpace(); |
| return adjustLineIndent(offset, documentModel, processor, indentOptions, model, whiteSpace, |
| blockAfterOffset != null ? blockAfterOffset.getNode() : null); |
| } |
| catch (FormattingModelInconsistencyException e) { |
| LOG.error(e); |
| } |
| finally { |
| enableFormatting(); |
| } |
| 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) |
| { |
| 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, |
| CommonCodeStyleSettings.IndentOptions indentOptions, |
| @Nullable FormatTextRanges affectedRanges, |
| int interestingOffset) |
| { |
| FormatProcessor processor = new FormatProcessor( |
| docModel, rootBlock, settings, indentOptions, affectedRanges, interestingOffset, FormattingProgressCallback.EMPTY |
| ); |
| while (!processor.iteration()) ; |
| return processor; |
| } |
| |
| private static int adjustLineIndent( |
| final int offset, |
| final FormattingDocumentModel documentModel, |
| final FormatProcessor processor, |
| final CommonCodeStyleSettings.IndentOptions indentOptions, |
| final FormattingModel model, |
| final WhiteSpace whiteSpace, |
| ASTNode nodeAfter) |
| { |
| boolean wsContainsCaret = whiteSpace.getStartOffset() <= offset && offset < whiteSpace.getEndOffset(); |
| |
| int lineStartOffset = getLineStartOffset(offset, whiteSpace, documentModel); |
| |
| final IndentInfo indent = calcIndent(offset, documentModel, processor, whiteSpace); |
| |
| final String newWS = whiteSpace.generateWhiteSpace(indentOptions, lineStartOffset, indent).toString(); |
| if (!whiteSpace.equalsToString(newWS)) { |
| try { |
| if (model instanceof FormattingModelEx) { |
| ((FormattingModelEx) model).replaceWhiteSpace(whiteSpace.getTextRange(), nodeAfter, newWS); |
| } |
| else { |
| model.replaceWhiteSpace(whiteSpace.getTextRange(), newWS); |
| } |
| } |
| finally { |
| model.commitChanges(); |
| } |
| } |
| |
| final int defaultOffset = offset - whiteSpace.getLength() + newWS.length(); |
| |
| if (wsContainsCaret) { |
| final int ws = whiteSpace.getStartOffset() |
| + CharArrayUtil.shiftForward(newWS, Math.max(0, lineStartOffset - whiteSpace.getStartOffset()), " \t"); |
| return Math.max(defaultOffset, ws); |
| } else { |
| return defaultOffset; |
| } |
| } |
| |
| private static boolean hasContentAfterLineBreak(final FormattingDocumentModel documentModel, final int offset, final WhiteSpace whiteSpace) { |
| return documentModel.getLineNumber(offset) == documentModel.getLineNumber(whiteSpace.getEndOffset()) && |
| documentModel.getTextLength() != offset; |
| } |
| |
| @Override |
| public String getLineIndent(final FormattingModel model, |
| final CodeStyleSettings settings, |
| final CommonCodeStyleSettings.IndentOptions indentOptions, |
| final int offset, |
| final TextRange affectedRange) { |
| final FormattingDocumentModel documentModel = model.getDocumentModel(); |
| final Block block = model.getRootBlock(); |
| if (block.getTextRange().isEmpty()) return null; // handing empty document case |
| final FormatProcessor processor = buildProcessorAndWrapBlocks( |
| documentModel, block, settings, indentOptions, new FormatTextRanges(affectedRange, true), offset |
| ); |
| final LeafBlockWrapper blockAfterOffset = processor.getBlockAfter(offset); |
| |
| if (blockAfterOffset != null) { |
| final WhiteSpace whiteSpace = blockAfterOffset.getWhiteSpace(); |
| final IndentInfo indent = calcIndent(offset, documentModel, processor, whiteSpace); |
| |
| return indent.generateNewWhiteSpace(indentOptions); |
| } |
| return null; |
| } |
| |
| private static IndentInfo calcIndent(int offset, FormattingDocumentModel documentModel, FormatProcessor processor, WhiteSpace whiteSpace) { |
| processor.setAllWhiteSpacesAreReadOnly(); |
| whiteSpace.setLineFeedsAreReadOnly(true); |
| final IndentInfo indent; |
| if (hasContentAfterLineBreak(documentModel, offset, whiteSpace)) { |
| whiteSpace.setReadOnly(false); |
| processor.formatWithoutRealModifications(); |
| indent = new IndentInfo(0, whiteSpace.getIndentOffset(), whiteSpace.getSpaces()); |
| } |
| else { |
| indent = processor.getIndentAt(offset); |
| } |
| return indent; |
| } |
| |
| public static String getText(final FormattingDocumentModel documentModel) { |
| return getCharSequence(documentModel).toString(); |
| } |
| |
| private static CharSequence getCharSequence(final FormattingDocumentModel documentModel) { |
| return documentModel.getText(new TextRange(0, documentModel.getTextLength())); |
| } |
| |
| private static int getLineStartOffset(final int offset, |
| final WhiteSpace whiteSpace, |
| final FormattingDocumentModel documentModel) { |
| int lineStartOffset = offset; |
| |
| CharSequence text = getCharSequence(documentModel); |
| lineStartOffset = CharArrayUtil.shiftBackwardUntil(text, lineStartOffset, " \t\n"); |
| if (lineStartOffset > whiteSpace.getStartOffset()) { |
| if (lineStartOffset >= text.length()) lineStartOffset = text.length() - 1; |
| final int wsStart = whiteSpace.getStartOffset(); |
| int prevEnd; |
| |
| if (text.charAt(lineStartOffset) == '\n' |
| && wsStart <= (prevEnd = documentModel.getLineStartOffset(documentModel.getLineNumber(lineStartOffset - 1))) && |
| documentModel.getText(new TextRange(prevEnd, lineStartOffset)).toString().trim().length() == 0 // ws consists of space only, it is not true for <![CDATA[ |
| ) { |
| lineStartOffset--; |
| } |
| lineStartOffset = CharArrayUtil.shiftBackward(text, lineStartOffset, "\t "); |
| if (lineStartOffset < 0) lineStartOffset = 0; |
| if (lineStartOffset != offset && text.charAt(lineStartOffset) == '\n') { |
| lineStartOffset++; |
| } |
| } |
| return lineStartOffset; |
| } |
| |
| @Override |
| public void adjustTextRange(final FormattingModel model, |
| final CodeStyleSettings settings, |
| final CommonCodeStyleSettings.IndentOptions indentOptions, |
| final TextRange affectedRange, |
| final boolean keepBlankLines, |
| final boolean keepLineBreaks, |
| final boolean changeWSBeforeFirstElement, |
| final boolean changeLineFeedsBeforeFirstElement, |
| @Nullable final IndentInfoStorage indentInfoStorage) { |
| disableFormatting(); |
| try { |
| validateModel(model); |
| final FormatProcessor processor = buildProcessorAndWrapBlocks( |
| model.getDocumentModel(), model.getRootBlock(), settings, indentOptions, new FormatTextRanges(affectedRange, true) |
| ); |
| LeafBlockWrapper current = processor.getFirstTokenBlock(); |
| while (current != null) { |
| WhiteSpace whiteSpace = current.getWhiteSpace(); |
| |
| if (!whiteSpace.isReadOnly()) { |
| if (whiteSpace.getStartOffset() > affectedRange.getStartOffset()) { |
| if (whiteSpace.containsLineFeeds() && indentInfoStorage != null) { |
| whiteSpace.setLineFeedsAreReadOnly(true); |
| current.setIndentFromParent(indentInfoStorage.getIndentInfo(current.getStartOffset())); |
| } |
| else { |
| whiteSpace.setReadOnly(true); |
| } |
| } |
| else { |
| if (!changeWSBeforeFirstElement) { |
| whiteSpace.setReadOnly(true); |
| } |
| else { |
| if (!changeLineFeedsBeforeFirstElement) { |
| whiteSpace.setLineFeedsAreReadOnly(true); |
| } |
| final SpacingImpl spaceProperty = current.getSpaceProperty(); |
| if (spaceProperty != null) { |
| boolean needChange = false; |
| int newKeepLineBreaks = spaceProperty.getKeepBlankLines(); |
| boolean newKeepLineBreaksFlag = spaceProperty.shouldKeepLineFeeds(); |
| |
| if (!keepLineBreaks) { |
| needChange = true; |
| newKeepLineBreaksFlag = false; |
| } |
| if (!keepBlankLines) { |
| needChange = true; |
| newKeepLineBreaks = 0; |
| } |
| |
| if (needChange) { |
| assert !(spaceProperty instanceof DependantSpacingImpl); |
| current.setSpaceProperty( |
| getSpacingImpl( |
| spaceProperty.getMinSpaces(), spaceProperty.getMaxSpaces(), spaceProperty.getMinLineFeeds(), |
| spaceProperty.isReadOnly(), |
| spaceProperty.isSafe(), newKeepLineBreaksFlag, newKeepLineBreaks, false, spaceProperty.getPrefLineFeeds() |
| ) |
| ); |
| } |
| } |
| } |
| } |
| } |
| current = current.getNextBlock(); |
| } |
| processor.format(model); |
| } |
| catch (FormattingModelInconsistencyException e) { |
| LOG.error(e); |
| } |
| finally { |
| enableFormatting(); |
| } |
| } |
| |
| @Override |
| public void adjustTextRange(final FormattingModel model, |
| final CodeStyleSettings settings, |
| final CommonCodeStyleSettings.IndentOptions indentOptions, |
| final TextRange affectedRange) { |
| disableFormatting(); |
| try { |
| validateModel(model); |
| final FormatProcessor processor = buildProcessorAndWrapBlocks( |
| model.getDocumentModel(), model.getRootBlock(), settings, indentOptions, new FormatTextRanges(affectedRange, true) |
| ); |
| LeafBlockWrapper current = processor.getFirstTokenBlock(); |
| while (current != null) { |
| WhiteSpace whiteSpace = current.getWhiteSpace(); |
| |
| if (!whiteSpace.isReadOnly()) { |
| if (whiteSpace.getStartOffset() > affectedRange.getStartOffset()) { |
| whiteSpace.setReadOnly(true); |
| } |
| else { |
| whiteSpace.setReadOnly(false); |
| } |
| } |
| current = current.getNextBlock(); |
| } |
| processor.format(model); |
| } |
| catch (FormattingModelInconsistencyException e) { |
| LOG.error(e); |
| } |
| finally { |
| enableFormatting(); |
| } |
| } |
| |
| @Override |
| public void saveIndents(final FormattingModel model, final TextRange affectedRange, |
| IndentInfoStorage storage, |
| final CodeStyleSettings settings, |
| final CommonCodeStyleSettings.IndentOptions indentOptions) { |
| try { |
| validateModel(model); |
| final Block block = model.getRootBlock(); |
| |
| final FormatProcessor processor = buildProcessorAndWrapBlocks( |
| model.getDocumentModel(), block, settings, indentOptions, new FormatTextRanges(affectedRange, true) |
| ); |
| LeafBlockWrapper current = processor.getFirstTokenBlock(); |
| while (current != null) { |
| WhiteSpace whiteSpace = current.getWhiteSpace(); |
| |
| if (!whiteSpace.isReadOnly() && whiteSpace.containsLineFeeds()) { |
| storage.saveIndentInfo(current.calcIndentFromParent(), current.getStartOffset()); |
| } |
| current = current.getNextBlock(); |
| } |
| } |
| catch (FormattingModelInconsistencyException e) { |
| LOG.error(e); |
| } |
| } |
| |
| @Override |
| public FormattingModel createFormattingModelForPsiFile(final PsiFile file, |
| @NotNull final Block rootBlock, |
| final CodeStyleSettings settings) { |
| return new PsiBasedFormattingModel(file, rootBlock, FormattingDocumentModelImpl.createOn(file)); |
| } |
| |
| @Override |
| public Indent getSpaceIndent(final int spaces, final boolean relative) { |
| return getIndent(Indent.Type.SPACES, spaces, relative, false); |
| } |
| |
| @Override |
| public Indent getIndent(@NotNull Indent.Type type, boolean relativeToDirectParent, boolean enforceIndentToChildren) { |
| return getIndent(type, 0, relativeToDirectParent, enforceIndentToChildren); |
| } |
| |
| @Override |
| public Indent getIndent(@NotNull Indent.Type type, int spaces, boolean relativeToDirectParent, boolean enforceIndentToChildren) { |
| return new IndentImpl(type, false, spaces, relativeToDirectParent, enforceIndentToChildren); |
| } |
| |
| @Override |
| public Indent getAbsoluteLabelIndent() { |
| return myAbsoluteLabelIndent; |
| } |
| |
| @Override |
| @NotNull |
| public Spacing createSafeSpacing(final boolean shouldKeepLineBreaks, final int keepBlankLines) { |
| return getSpacingImpl(0, 0, 0, false, true, shouldKeepLineBreaks, keepBlankLines, false, 0); |
| } |
| |
| @Override |
| @NotNull |
| public Spacing createKeepingFirstColumnSpacing(final int minSpace, |
| final int maxSpace, |
| final boolean keepLineBreaks, |
| final int keepBlankLines) { |
| return getSpacingImpl(minSpace, maxSpace, -1, false, false, keepLineBreaks, keepBlankLines, true, 0); |
| } |
| |
| @Override |
| @NotNull |
| public Spacing createSpacing(final int minSpaces, final int maxSpaces, final int minLineFeeds, final boolean keepLineBreaks, final int keepBlankLines, |
| final int prefLineFeeds) { |
| return getSpacingImpl(minSpaces, maxSpaces, minLineFeeds, false, false, keepLineBreaks, keepBlankLines, false, prefLineFeeds); |
| } |
| |
| private final Map<SpacingImpl,SpacingImpl> ourSharedProperties = new HashMap<SpacingImpl,SpacingImpl>(); |
| private final SpacingImpl ourSharedSpacing = new SpacingImpl(-1,-1,-1,false,false,false,-1,false,0); |
| |
| private SpacingImpl getSpacingImpl(final int minSpaces, |
| final int maxSpaces, |
| final int minLineFeeds, |
| final boolean readOnly, |
| final boolean safe, |
| final boolean keepLineBreaksFlag, |
| final int keepLineBreaks, |
| final boolean keepFirstColumn, |
| int prefLineFeeds) |
| { |
| synchronized(ourSharedSpacing) { |
| ourSharedSpacing.init(minSpaces, maxSpaces, minLineFeeds, readOnly, safe, keepLineBreaksFlag, keepLineBreaks, keepFirstColumn, prefLineFeeds); |
| SpacingImpl spacing = ourSharedProperties.get(ourSharedSpacing); |
| |
| if (spacing == null) { |
| spacing = new SpacingImpl(minSpaces, maxSpaces, minLineFeeds, readOnly, safe, keepLineBreaksFlag, keepLineBreaks, keepFirstColumn, prefLineFeeds); |
| ourSharedProperties.put(spacing, spacing); |
| } |
| return spacing; |
| } |
| } |
| |
| @Override |
| public Indent getAbsoluteNoneIndent() { |
| return myAbsoluteNoneIndent; |
| } |
| |
| @Override |
| public Indent getLabelIndent() { |
| return myLabelIndent; |
| } |
| |
| @Override |
| public Indent getContinuationIndent(boolean relative) { |
| return relative ? myContinuationIndentRelativeToDirectParent : myContinuationIndentNotRelativeToDirectParent; |
| } |
| |
| //is default |
| @Override |
| public Indent getContinuationWithoutFirstIndent(boolean relative) { |
| return relative ? myContinuationWithoutFirstIndentRelativeToDirectParent : myContinuationWithoutFirstIndentNotRelativeToDirectParent; |
| } |
| |
| @Override |
| public boolean isDisabled() { |
| return myIsDisabledCount.get() > 0; |
| } |
| |
| private void disableFormatting() { |
| myIsDisabledCount.incrementAndGet(); |
| } |
| |
| private void enableFormatting() { |
| int old = myIsDisabledCount.getAndDecrement(); |
| if (old <= 0) { |
| LOG.error("enableFormatting()/disableFormatting() not paired. DisabledLevel = " + old); |
| } |
| } |
| |
| @Nullable |
| public <T> T runWithFormattingDisabled(@NotNull Computable<T> runnable) { |
| disableFormatting(); |
| try { |
| return runnable.compute(); |
| } |
| finally { |
| enableFormatting(); |
| } |
| } |
| |
| private abstract static class MyFormattingTask implements SequentialTask { |
| private FormatProcessor myProcessor; |
| private boolean myDone; |
| |
| @Override |
| public void prepare() { |
| myProcessor = buildProcessor(); |
| } |
| |
| @Override |
| public boolean isDone() { |
| return myDone; |
| } |
| |
| @Override |
| public boolean iteration() { |
| return myDone = myProcessor.iteration(); |
| } |
| |
| @Override |
| public void stop() { |
| myProcessor.stopSequentialProcessing(); |
| myDone = true; |
| } |
| |
| @NotNull |
| protected abstract FormatProcessor buildProcessor(); |
| } |
| |
| private static void validateModel(FormattingModel model) throws FormattingModelInconsistencyException { |
| FormattingDocumentModel documentModel = model.getDocumentModel(); |
| Document document = documentModel.getDocument(); |
| Block rootBlock = model.getRootBlock(); |
| if (rootBlock instanceof ASTBlock) { |
| PsiElement rootElement = ((ASTBlock)rootBlock).getNode().getPsi(); |
| if (!rootElement.isValid()) { |
| throw new FormattingModelInconsistencyException("Invalid root block PSI element"); |
| } |
| PsiFile file = rootElement.getContainingFile(); |
| Project project = file.getProject(); |
| PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project); |
| if (documentManager.isUncommited(document)) { |
| throw new FormattingModelInconsistencyException("Uncommitted document"); |
| } |
| if (document.getTextLength() != file.getTextLength()) { |
| throw new FormattingModelInconsistencyException( |
| "Document length " + document.getTextLength() + |
| " doesn't match PSI file length " + file.getTextLength() + ", language: " + file.getLanguage() |
| ); |
| } |
| } |
| } |
| |
| private static class FormattingModelInconsistencyException extends Exception { |
| public FormattingModelInconsistencyException(String message) { |
| super(message); |
| } |
| } |
| } |