com.intellij.psi.impl.source.codeStyle.CodeFormatterFacade.java Source code

Java tutorial

Introduction

Here is the source code for com.intellij.psi.impl.source.codeStyle.CodeFormatterFacade.java

Source

/*
 * Copyright 2000-2012 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.intellij.psi.impl.source.codeStyle;

import com.intellij.formatting.*;
import com.intellij.ide.DataManager;
import com.intellij.injected.editor.DocumentWindow;
import com.intellij.lang.ASTNode;
import com.intellij.lang.Language;
import com.intellij.lang.LanguageFormatting;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.actionSystem.EditorActionManager;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Segment;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.UserDataHolder;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.psi.formatter.DocumentBasedFormattingModel;
import com.intellij.psi.impl.source.PostprocessReformattingAspect;
import com.intellij.psi.impl.source.SourceTreeToPsiMap;
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtilBase;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.ContainerUtilRt;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.awt.*;
import java.util.*;
import java.util.List;

public class CodeFormatterFacade {

    private static final Logger LOG = Logger
            .getInstance("#com.intellij.psi.impl.source.codeStyle.CodeFormatterFacade");

    private static final String WRAP_LINE_COMMAND_NAME = "AutoWrapLongLine";

    /**
     * This key is used as a flag that indicates if <code>'wrap long line during formatting'</code> activity is performed now.
     *
     * @see CodeStyleSettings#WRAP_LONG_LINES
     */
    public static final Key<Boolean> WRAP_LONG_LINE_DURING_FORMATTING_IN_PROGRESS_KEY = new Key<Boolean>(
            "WRAP_LONG_LINE_DURING_FORMATTING_IN_PROGRESS_KEY");

    private final CodeStyleSettings mySettings;
    private final FormatterTagHandler myTagHandler;
    private final int myRightMargin;

    public CodeFormatterFacade(CodeStyleSettings settings, @Nullable Language language) {
        mySettings = settings;
        myTagHandler = new FormatterTagHandler(settings);
        myRightMargin = mySettings.getRightMargin(language);
    }

    public ASTNode processElement(ASTNode element) {
        TextRange range = element.getTextRange();
        return processRange(element, range.getStartOffset(), range.getEndOffset());
    }

    public ASTNode processRange(final ASTNode element, final int startOffset, final int endOffset) {
        return doProcessRange(element, startOffset, endOffset, null);
    }

    /**
     * rangeMarker will be disposed
     */
    public ASTNode processRange(@NotNull ASTNode element, @NotNull RangeMarker rangeMarker) {
        return doProcessRange(element, rangeMarker.getStartOffset(), rangeMarker.getEndOffset(), rangeMarker);
    }

    private ASTNode doProcessRange(final ASTNode element, final int startOffset, final int endOffset,
            @Nullable RangeMarker rangeMarker) {
        final PsiElement psiElement = SourceTreeToPsiMap.treeElementToPsi(element);
        assert psiElement != null;
        final PsiFile file = psiElement.getContainingFile();
        final Document document = file.getViewProvider().getDocument();

        PsiElement elementToFormat = document instanceof DocumentWindow
                ? InjectedLanguageManager.getInstance(file.getProject()).getTopLevelFile(file)
                : psiElement;
        final PsiFile fileToFormat = elementToFormat.getContainingFile();

        final FormattingModelBuilder builder = LanguageFormatting.INSTANCE.forContext(fileToFormat);
        if (builder != null) {
            if (rangeMarker == null && document != null && endOffset < document.getTextLength()) {
                rangeMarker = document.createRangeMarker(startOffset, endOffset);
            }

            TextRange range = preprocess(element, TextRange.create(startOffset, endOffset));
            if (document instanceof DocumentWindow) {
                DocumentWindow documentWindow = (DocumentWindow) document;
                range = documentWindow.injectedToHost(range);
            }

            //final SmartPsiElementPointer pointer = SmartPointerManager.getInstance(psiElement.getProject()).createSmartPsiElementPointer(psiElement);
            final FormattingModel model = CoreFormatterUtil.buildModel(builder, elementToFormat, mySettings,
                    FormattingMode.REFORMAT);
            if (file.getTextLength() > 0) {
                try {
                    FormatterEx.getInstanceEx().format(model, mySettings,
                            mySettings.getIndentOptionsByFile(fileToFormat, range),
                            new FormatTextRanges(range, true));

                    wrapLongLinesIfNecessary(file, document, startOffset, endOffset);
                } catch (IncorrectOperationException e) {
                    LOG.error(e);
                }
            }

            if (!psiElement.isValid()) {
                if (rangeMarker != null) {
                    final PsiElement at = file.findElementAt(rangeMarker.getStartOffset());
                    final PsiElement result = PsiTreeUtil.getParentOfType(at, psiElement.getClass(), false);
                    assert result != null;
                    rangeMarker.dispose();
                    return result.getNode();
                } else {
                    assert false;
                }
            }
            //      return SourceTreeToPsiMap.psiElementToTree(pointer.getElement());
        }

        if (rangeMarker != null) {
            rangeMarker.dispose();
        }
        return element;
    }

    public void processText(PsiFile file, final FormatTextRanges ranges, boolean doPostponedFormatting) {
        final Project project = file.getProject();
        Document document = PsiDocumentManager.getInstance(project).getDocument(file);
        final List<FormatTextRanges.FormatTextRange> textRanges = ranges.getRanges();
        if (document instanceof DocumentWindow) {
            file = InjectedLanguageManager.getInstance(file.getProject()).getTopLevelFile(file);
            final DocumentWindow documentWindow = (DocumentWindow) document;
            for (FormatTextRanges.FormatTextRange range : textRanges) {
                range.setTextRange(documentWindow.injectedToHost(range.getTextRange()));
            }
            document = documentWindow.getDelegate();
        }

        final FormattingModelBuilder builder = LanguageFormatting.INSTANCE.forContext(file);
        final Language contextLanguage = file.getLanguage();

        if (builder != null) {
            if (file.getTextLength() > 0) {
                LOG.assertTrue(document != null);
                try {
                    final FileViewProvider viewProvider = file.getViewProvider();
                    final PsiElement startElement = viewProvider
                            .findElementAt(textRanges.get(0).getTextRange().getStartOffset(), contextLanguage);
                    final PsiElement endElement = viewProvider.findElementAt(
                            textRanges.get(textRanges.size() - 1).getTextRange().getEndOffset() - 1,
                            contextLanguage);
                    final PsiElement commonParent = startElement != null && endElement != null
                            ? PsiTreeUtil.findCommonParent(startElement, endElement)
                            : null;
                    ASTNode node = null;
                    if (commonParent != null) {
                        node = commonParent.getNode();
                    }
                    if (node == null) {
                        node = file.getNode();
                    }
                    for (FormatTextRanges.FormatTextRange range : ranges.getRanges()) {
                        TextRange rangeToUse = preprocess(node, range.getTextRange());
                        range.setTextRange(rangeToUse);
                    }
                    if (doPostponedFormatting) {
                        RangeMarker[] markers = new RangeMarker[textRanges.size()];
                        int i = 0;
                        for (FormatTextRanges.FormatTextRange range : textRanges) {
                            TextRange textRange = range.getTextRange();
                            int start = textRange.getStartOffset();
                            int end = textRange.getEndOffset();
                            if (start >= 0 && end > start && end <= document.getTextLength()) {
                                markers[i] = document.createRangeMarker(textRange);
                                markers[i].setGreedyToLeft(true);
                                markers[i].setGreedyToRight(true);
                                i++;
                            }
                        }
                        final PostprocessReformattingAspect component = file.getProject()
                                .getComponent(PostprocessReformattingAspect.class);
                        FormattingProgressTask.FORMATTING_CANCELLED_FLAG.set(false);
                        component.doPostponedFormatting(file.getViewProvider());
                        i = 0;
                        for (FormatTextRanges.FormatTextRange range : textRanges) {
                            RangeMarker marker = markers[i];
                            if (marker != null) {
                                range.setTextRange(TextRange.create(marker));
                                marker.dispose();
                            }
                            i++;
                        }
                    }
                    if (FormattingProgressTask.FORMATTING_CANCELLED_FLAG.get()) {
                        return;
                    }

                    final FormattingModel originalModel = CoreFormatterUtil.buildModel(builder, file, mySettings,
                            FormattingMode.REFORMAT);
                    final FormattingModel model = new DocumentBasedFormattingModel(originalModel.getRootBlock(),
                            document, project, mySettings, file.getFileType(), file);

                    FormatterEx formatter = FormatterEx.getInstanceEx();
                    if (CodeStyleManager.getInstance(project).isSequentialProcessingAllowed()) {
                        formatter.setProgressTask(new FormattingProgressTask(project, file, document));
                    }

                    CommonCodeStyleSettings.IndentOptions indentOptions = null;
                    if (builder instanceof FormattingModelBuilderEx) {
                        indentOptions = ((FormattingModelBuilderEx) builder).getIndentOptionsToUse(file, ranges,
                                mySettings);
                    }
                    if (indentOptions == null) {
                        indentOptions = mySettings.getIndentOptionsByFile(file,
                                textRanges.size() == 1 ? textRanges.get(0).getTextRange() : null);
                    }

                    formatter.format(model, mySettings, indentOptions, ranges);
                    for (FormatTextRanges.FormatTextRange range : textRanges) {
                        TextRange textRange = range.getTextRange();
                        wrapLongLinesIfNecessary(file, document, textRange.getStartOffset(),
                                textRange.getEndOffset());
                    }
                } catch (IncorrectOperationException e) {
                    LOG.error(e);
                }
            }
        }
    }

    private TextRange preprocess(@NotNull final ASTNode node, @NotNull TextRange range) {
        TextRange result = range;
        PsiElement psi = node.getPsi();
        if (!psi.isValid()) {
            return result;
        }

        PsiFile file = psi.getContainingFile();

        // We use a set here because we encountered a situation when more than one PSI leaf points to the same injected fragment
        // (at least for sql injected into sql).
        final LinkedHashSet<TextRange> injectedFileRangesSet = ContainerUtilRt.newLinkedHashSet();

        if (!psi.getProject().isDefault()) {
            List<DocumentWindow> injectedDocuments = InjectedLanguageUtil.getCachedInjectedDocuments(file);
            if (!injectedDocuments.isEmpty()) {
                for (DocumentWindow injectedDocument : injectedDocuments) {
                    injectedFileRangesSet.add(
                            TextRange.from(injectedDocument.injectedToHost(0), injectedDocument.getTextLength()));
                }
            } else {
                Collection<PsiLanguageInjectionHost> injectionHosts = collectInjectionHosts(file, range);
                PsiLanguageInjectionHost.InjectedPsiVisitor visitor = new PsiLanguageInjectionHost.InjectedPsiVisitor() {
                    @Override
                    public void visit(@NotNull PsiFile injectedPsi,
                            @NotNull List<PsiLanguageInjectionHost.Shred> places) {
                        for (PsiLanguageInjectionHost.Shred place : places) {
                            Segment rangeMarker = place.getHostRangeMarker();
                            injectedFileRangesSet.add(
                                    TextRange.create(rangeMarker.getStartOffset(), rangeMarker.getEndOffset()));
                        }
                    }
                };
                for (PsiLanguageInjectionHost host : injectionHosts) {
                    InjectedLanguageUtil.enumerate(host, visitor);
                }
            }
        }

        if (!injectedFileRangesSet.isEmpty()) {
            List<TextRange> ranges = ContainerUtilRt.newArrayList(injectedFileRangesSet);
            Collections.reverse(ranges);
            for (TextRange injectedFileRange : ranges) {
                int startHostOffset = injectedFileRange.getStartOffset();
                int endHostOffset = injectedFileRange.getEndOffset();
                if (startHostOffset >= range.getStartOffset() && endHostOffset <= range.getEndOffset()) {
                    PsiFile injected = InjectedLanguageUtil.findInjectedPsiNoCommit(file, startHostOffset);
                    if (injected != null) {
                        int startInjectedOffset = range.getStartOffset() > startHostOffset
                                ? startHostOffset - range.getStartOffset()
                                : 0;
                        int endInjectedOffset = injected.getTextLength();
                        if (range.getEndOffset() < endHostOffset) {
                            endInjectedOffset -= endHostOffset - range.getEndOffset();
                        }
                        final TextRange initialInjectedRange = TextRange.create(startInjectedOffset,
                                endInjectedOffset);
                        TextRange injectedRange = initialInjectedRange;
                        for (PreFormatProcessor processor : Extensions.getExtensions(PreFormatProcessor.EP_NAME)) {
                            injectedRange = processor.process(injected.getNode(), injectedRange);
                        }

                        // Allow only range expansion (not reduction) for injected context.
                        if ((initialInjectedRange.getStartOffset() > injectedRange.getStartOffset()
                                && initialInjectedRange.getStartOffset() > 0)
                                || (initialInjectedRange.getEndOffset() < injectedRange.getEndOffset()
                                        && initialInjectedRange.getEndOffset() < injected.getTextLength())) {
                            range = TextRange.create(
                                    range.getStartOffset() + injectedRange.getStartOffset()
                                            - initialInjectedRange.getStartOffset(),
                                    range.getEndOffset() + initialInjectedRange.getEndOffset()
                                            - injectedRange.getEndOffset());
                        }
                    }
                }
            }
        }

        if (!mySettings.FORMATTER_TAGS_ENABLED) {
            for (PreFormatProcessor processor : Extensions.getExtensions(PreFormatProcessor.EP_NAME)) {
                result = processor.process(node, result);
            }
        } else {
            result = preprocessEnabledRanges(node, result);
        }

        return result;
    }

    private TextRange preprocessEnabledRanges(@NotNull final ASTNode node, @NotNull TextRange range) {
        TextRange result = TextRange.create(range.getStartOffset(), range.getEndOffset());
        List<TextRange> enabledRanges = myTagHandler.getEnabledRanges(node, result);
        int delta = 0;
        for (TextRange enabledRange : enabledRanges) {
            enabledRange = enabledRange.shiftRight(delta);
            for (PreFormatProcessor processor : Extensions.getExtensions(PreFormatProcessor.EP_NAME)) {
                TextRange processedRange = processor.process(node, enabledRange);
                delta += processedRange.getLength() - enabledRange.getLength();
            }
        }
        result = result.grown(delta);
        return result;
    }

    @NotNull
    private static Collection<PsiLanguageInjectionHost> collectInjectionHosts(@NotNull PsiFile file,
            @NotNull TextRange range) {
        Stack<PsiElement> toProcess = new Stack<PsiElement>();
        for (PsiElement e = file.findElementAt(range.getStartOffset()); e != null; e = e.getNextSibling()) {
            if (e.getTextRange().getStartOffset() >= range.getEndOffset()) {
                break;
            }
            toProcess.push(e);
        }
        if (toProcess.isEmpty()) {
            return Collections.emptySet();
        }
        Set<PsiLanguageInjectionHost> result = null;
        while (!toProcess.isEmpty()) {
            PsiElement e = toProcess.pop();
            if (e instanceof PsiLanguageInjectionHost) {
                if (result == null) {
                    result = ContainerUtilRt.newHashSet();
                }
                result.add((PsiLanguageInjectionHost) e);
            } else {
                for (PsiElement child = e.getFirstChild(); child != null; child = child.getNextSibling()) {
                    if (e.getTextRange().getStartOffset() >= range.getEndOffset()) {
                        break;
                    }
                    toProcess.push(child);
                }
            }
        }
        return result == null ? Collections.<PsiLanguageInjectionHost>emptySet() : result;
    }

    /**
     * Inspects all lines of the given document and wraps all of them that exceed {@link CodeStyleSettings#getRightMargin(com.intellij.lang.Language)}
     * right margin}.
     * <p/>
     * I.e. the algorithm is to do the following for every line:
     * <p/>
     * <pre>
     * <ol>
     *   <li>
     *      Check if the line exceeds {@link CodeStyleSettings#getRightMargin(com.intellij.lang.Language)}  right margin}. Go to the next line in the case of
     *      negative answer;
     *   </li>
     *   <li>Determine line wrap position; </li>
     *   <li>
     *      Perform 'smart wrap', i.e. not only wrap the line but insert additional characters over than line feed if necessary.
     *      For example consider that we wrap a single-line comment - we need to insert comment symbols on a start of the wrapped
     *      part as well. Generally, we get the same behavior as during pressing 'Enter' at wrap position during editing document;
     *   </li>
     * </ol>
     </pre>
     *
     * @param file        file that holds parsed document tree
     * @param document    target document
     * @param startOffset start offset of the first line to check for wrapping (inclusive)
     * @param endOffset   end offset of the first line to check for wrapping (exclusive)
     */
    private void wrapLongLinesIfNecessary(@NotNull final PsiFile file, @Nullable final Document document,
            final int startOffset, final int endOffset) {
        if (!mySettings.getCommonSettings(file.getLanguage()).WRAP_LONG_LINES || PostprocessReformattingAspect
                .getInstance(file.getProject()).isViewProviderLocked(file.getViewProvider()) || document == null) {
            return;
        }

        final VirtualFile vFile = FileDocumentManager.getInstance().getFile(document);
        if ((vFile == null || vFile instanceof LightVirtualFile)
                && !ApplicationManager.getApplication().isUnitTestMode()) {
            // we assume that control flow reaches this place when the document is backed by a "virtual" file so any changes made by
            // a formatter affect only PSI and it is out of sync with a document text
            return;
        }

        Editor editor = PsiUtilBase.findEditor(file);
        EditorFactory editorFactory = null;
        if (editor == null) {
            if (!ApplicationManager.getApplication().isDispatchThread()) {
                return;
            }
            editorFactory = EditorFactory.getInstance();
            editor = editorFactory.createEditor(document, file.getProject());
        }
        try {
            final Editor editorToUse = editor;
            ApplicationManager.getApplication().runWriteAction(new Runnable() {
                @Override
                public void run() {
                    final CaretModel caretModel = editorToUse.getCaretModel();
                    final int caretOffset = caretModel.getOffset();
                    final RangeMarker caretMarker = editorToUse.getDocument().createRangeMarker(caretOffset,
                            caretOffset);
                    doWrapLongLinesIfNecessary(editorToUse, file.getProject(), editorToUse.getDocument(),
                            startOffset, endOffset);
                    if (caretMarker.isValid() && caretModel.getOffset() != caretMarker.getStartOffset()) {
                        caretModel.moveToOffset(caretMarker.getStartOffset());
                    }
                }
            });
        } finally {
            PsiDocumentManager documentManager = PsiDocumentManager.getInstance(file.getProject());
            if (documentManager.isUncommited(document))
                documentManager.commitDocument(document);
            if (editorFactory != null) {
                editorFactory.releaseEditor(editor);
            }
        }
    }

    public void doWrapLongLinesIfNecessary(@NotNull final Editor editor, @NotNull final Project project,
            @NotNull Document document, int startOffset, int endOffset) {
        // Normalization.
        int startOffsetToUse = Math.min(document.getTextLength(), Math.max(0, startOffset));
        int endOffsetToUse = Math.min(document.getTextLength(), Math.max(0, endOffset));

        LineWrapPositionStrategy strategy = LanguageLineWrapPositionStrategy.INSTANCE.forEditor(editor);
        CharSequence text = document.getCharsSequence();
        int startLine = document.getLineNumber(startOffsetToUse);
        int endLine = document.getLineNumber(Math.max(0, endOffsetToUse - 1));
        int maxLine = Math.min(document.getLineCount(), endLine + 1);
        int tabSize = EditorUtil.getTabSize(editor);
        if (tabSize <= 0) {
            tabSize = 1;
        }
        int spaceSize = EditorUtil.getSpaceWidth(Font.PLAIN, editor);
        int[] shifts = new int[2];
        // shifts[0] - lines shift.
        // shift[1] - offset shift.

        for (int line = startLine; line < maxLine; line++) {
            int startLineOffset = document.getLineStartOffset(line);
            int endLineOffset = document.getLineEndOffset(line);
            final int preferredWrapPosition = calculatePreferredWrapPosition(editor, text, tabSize, spaceSize,
                    startLineOffset, endLineOffset, endOffsetToUse);

            if (preferredWrapPosition < 0 || preferredWrapPosition >= endLineOffset) {
                continue;
            }
            if (preferredWrapPosition >= endOffsetToUse) {
                return;
            }

            // We know that current line exceeds right margin if control flow reaches this place, so, wrap it.
            int wrapOffset = strategy.calculateWrapPosition(document, editor.getProject(),
                    Math.max(startLineOffset, startOffsetToUse), Math.min(endLineOffset, endOffsetToUse),
                    preferredWrapPosition, false, false);
            if (wrapOffset < 0 // No appropriate wrap position is found.
                    // No point in splitting line when its left part contains only white spaces, example:
                    //    line start -> |                   | <- right margin
                    //                  |   aaaaaaaaaaaaaaaa|aaaaaaaaaaaaaaaaaaaa() <- don't want to wrap this line even if it exceeds right margin
                    || CharArrayUtil.shiftBackward(text, startLineOffset, wrapOffset - 1,
                            " \t") < startLineOffset) {
                continue;
            }

            // Move caret to the target position and emulate pressing <enter>.
            editor.getCaretModel().moveToOffset(wrapOffset);
            emulateEnter(editor, project, shifts);

            //If number of inserted symbols on new line after wrapping more or equal then symbols left on previous line
            //there was no point to wrapping it, so reverting to before wrapping version
            if (shifts[1] - 1 >= wrapOffset - startLineOffset) {
                document.deleteString(wrapOffset, wrapOffset + shifts[1]);
            } else {
                // We know that number of lines is just increased, hence, update the data accordingly.
                maxLine += shifts[0];
                endOffsetToUse += shifts[1];
            }

        }
    }

    /**
     * Emulates pressing <code>Enter</code> at current caret position.
     *
     * @param editor       target editor
     * @param project      target project
     * @param shifts       two-elements array which is expected to be filled with the following info:
     *                       1. The first element holds added lines number;
     *                       2. The second element holds added symbols number;
     */
    private static void emulateEnter(@NotNull final Editor editor, @NotNull Project project, int[] shifts) {
        final DataContext dataContext = prepareContext(editor.getComponent(), project);
        int caretOffset = editor.getCaretModel().getOffset();
        Document document = editor.getDocument();
        SelectionModel selectionModel = editor.getSelectionModel();
        int startSelectionOffset = 0;
        int endSelectionOffset = 0;
        boolean restoreSelection = selectionModel.hasSelection();
        if (restoreSelection) {
            startSelectionOffset = selectionModel.getSelectionStart();
            endSelectionOffset = selectionModel.getSelectionEnd();
            selectionModel.removeSelection();
        }
        int textLengthBeforeWrap = document.getTextLength();
        int lineCountBeforeWrap = document.getLineCount();

        DataManager.getInstance().saveInDataContext(dataContext, WRAP_LONG_LINE_DURING_FORMATTING_IN_PROGRESS_KEY,
                true);
        CommandProcessor commandProcessor = CommandProcessor.getInstance();
        try {
            Runnable command = new Runnable() {
                @Override
                public void run() {
                    EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_ENTER)
                            .execute(editor, dataContext);
                }
            };
            if (commandProcessor.getCurrentCommand() == null) {
                commandProcessor.executeCommand(editor.getProject(), command, WRAP_LINE_COMMAND_NAME, null);
            } else {
                command.run();
            }
        } finally {
            DataManager.getInstance().saveInDataContext(dataContext,
                    WRAP_LONG_LINE_DURING_FORMATTING_IN_PROGRESS_KEY, null);
        }
        int symbolsDiff = document.getTextLength() - textLengthBeforeWrap;
        if (restoreSelection) {
            int newSelectionStart = startSelectionOffset;
            int newSelectionEnd = endSelectionOffset;
            if (startSelectionOffset >= caretOffset) {
                newSelectionStart += symbolsDiff;
            }
            if (endSelectionOffset >= caretOffset) {
                newSelectionEnd += symbolsDiff;
            }
            selectionModel.setSelection(newSelectionStart, newSelectionEnd);
        }
        shifts[0] = document.getLineCount() - lineCountBeforeWrap;
        shifts[1] = symbolsDiff;
    }

    /**
     * Checks if it's worth to try to wrap target line (it's long enough) and tries to calculate preferred wrap position.
     *
     * @param editor                target editor
     * @param text                  text contained at the given editor
     * @param tabSize               tab space to use (number of visual columns occupied by a tab)
     * @param spaceSize             space width in pixels
     * @param startLineOffset       start offset of the text line to process
     * @param endLineOffset         end offset of the text line to process
     * @param targetRangeEndOffset  target text region's end offset
     * @return                      negative value if no wrapping should be performed for the target line;
     *                              preferred wrap position otherwise
     */
    private int calculatePreferredWrapPosition(@NotNull Editor editor, @NotNull CharSequence text, int tabSize,
            int spaceSize, int startLineOffset, int endLineOffset, int targetRangeEndOffset) {
        boolean hasTabs = false;
        boolean canOptimize = true;
        boolean hasNonSpaceSymbols = false;
        loop: for (int i = startLineOffset; i < Math.min(endLineOffset, targetRangeEndOffset); i++) {
            char c = text.charAt(i);
            switch (c) {
            case '\t': {
                hasTabs = true;
                if (hasNonSpaceSymbols) {
                    canOptimize = false;
                    break loop;
                }
            }
            case ' ':
                break;
            default:
                hasNonSpaceSymbols = true;
            }
        }

        if (!hasTabs) {
            return wrapPositionForTextWithoutTabs(startLineOffset, endLineOffset, targetRangeEndOffset);
        } else if (canOptimize) {
            return wrapPositionForTabbedTextWithOptimization(text, tabSize, startLineOffset, endLineOffset,
                    targetRangeEndOffset);
        } else {
            return wrapPositionForTabbedTextWithoutOptimization(editor, text, spaceSize, startLineOffset,
                    endLineOffset, targetRangeEndOffset);
        }
    }

    private int wrapPositionForTextWithoutTabs(int startLineOffset, int endLineOffset, int targetRangeEndOffset) {
        if (Math.min(endLineOffset, targetRangeEndOffset) - startLineOffset > myRightMargin) {
            return startLineOffset + myRightMargin - FormatConstants.RESERVED_LINE_WRAP_WIDTH_IN_COLUMNS;
        }
        return -1;
    }

    private int wrapPositionForTabbedTextWithOptimization(@NotNull CharSequence text, int tabSize,
            int startLineOffset, int endLineOffset, int targetRangeEndOffset) {
        int width = 0;
        int symbolWidth;
        int result = Integer.MAX_VALUE;
        boolean wrapLine = false;
        for (int i = startLineOffset; i < Math.min(endLineOffset, targetRangeEndOffset); i++) {
            char c = text.charAt(i);
            switch (c) {
            case '\t':
                symbolWidth = tabSize - (width % tabSize);
                break;
            default:
                symbolWidth = 1;
            }
            if (width + symbolWidth + FormatConstants.RESERVED_LINE_WRAP_WIDTH_IN_COLUMNS >= myRightMargin
                    && (Math.min(endLineOffset, targetRangeEndOffset)
                            - i) >= FormatConstants.RESERVED_LINE_WRAP_WIDTH_IN_COLUMNS) {
                // Remember preferred position.
                result = i - 1;
            }
            if (width + symbolWidth >= myRightMargin) {
                wrapLine = true;
                break;
            }
            width += symbolWidth;
        }
        return wrapLine ? result : -1;
    }

    private int wrapPositionForTabbedTextWithoutOptimization(@NotNull Editor editor, @NotNull CharSequence text,
            int spaceSize, int startLineOffset, int endLineOffset, int targetRangeEndOffset) {
        int width = 0;
        int x = 0;
        int newX;
        int symbolWidth;
        int result = Integer.MAX_VALUE;
        boolean wrapLine = false;
        for (int i = startLineOffset; i < Math.min(endLineOffset, targetRangeEndOffset); i++) {
            char c = text.charAt(i);
            switch (c) {
            case '\t':
                newX = EditorUtil.nextTabStop(x, editor);
                int diffInPixels = newX - x;
                symbolWidth = diffInPixels / spaceSize;
                if (diffInPixels % spaceSize > 0) {
                    symbolWidth++;
                }
                break;
            default:
                newX = x + EditorUtil.charWidth(c, Font.PLAIN, editor);
                symbolWidth = 1;
            }
            if (width + symbolWidth + FormatConstants.RESERVED_LINE_WRAP_WIDTH_IN_COLUMNS >= myRightMargin
                    && (Math.min(endLineOffset, targetRangeEndOffset)
                            - i) >= FormatConstants.RESERVED_LINE_WRAP_WIDTH_IN_COLUMNS) {
                result = i - 1;
            }
            if (width + symbolWidth >= myRightMargin) {
                wrapLine = true;
                break;
            }
            x = newX;
            width += symbolWidth;
        }
        return wrapLine ? result : -1;
    }

    @NotNull
    private static DataContext prepareContext(@NotNull Component component, @NotNull final Project project) {
        // There is a possible case that formatting is performed from project view and editor is not opened yet. The problem is that
        // its data context doesn't contain information about project then. So, we explicitly support that here (see IDEA-72791).
        final DataContext baseDataContext = DataManager.getInstance().getDataContext(component);
        return new DelegatingDataContext(baseDataContext) {
            @Override
            public Object getData(@NonNls String dataId) {
                Object result = baseDataContext.getData(dataId);
                if (result == null && CommonDataKeys.PROJECT.is(dataId)) {
                    result = project;
                }
                return result;
            }
        };
    }

    private static class DelegatingDataContext implements DataContext, UserDataHolder {

        private final DataContext myDataContextDelegate;
        private final UserDataHolder myDataHolderDelegate;

        DelegatingDataContext(DataContext delegate) {
            myDataContextDelegate = delegate;
            if (delegate instanceof UserDataHolder) {
                myDataHolderDelegate = (UserDataHolder) delegate;
            } else {
                myDataHolderDelegate = null;
            }
        }

        @Override
        public Object getData(@NonNls String dataId) {
            return myDataContextDelegate.getData(dataId);
        }

        @Override
        public <T> T getUserData(@NotNull Key<T> key) {
            return myDataHolderDelegate == null ? null : myDataHolderDelegate.getUserData(key);
        }

        @Override
        public <T> void putUserData(@NotNull Key<T> key, @Nullable T value) {
            if (myDataHolderDelegate != null) {
                myDataHolderDelegate.putUserData(key, value);
            }
        }
    }
}