CPP-3103: Conditionally uncompiled code unexpectedly formatted
[idea/community.git] / platform / lang-impl / src / com / intellij / psi / impl / source / codeStyle / CodeFormatterFacade.java
1 /*
2  * Copyright 2000-2012 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.intellij.psi.impl.source.codeStyle;
18
19 import com.intellij.formatting.*;
20 import com.intellij.ide.DataManager;
21 import com.intellij.injected.editor.DocumentWindow;
22 import com.intellij.lang.ASTNode;
23 import com.intellij.lang.Language;
24 import com.intellij.lang.LanguageFormatting;
25 import com.intellij.lang.injection.InjectedLanguageManager;
26 import com.intellij.openapi.actionSystem.CommonDataKeys;
27 import com.intellij.openapi.actionSystem.DataContext;
28 import com.intellij.openapi.actionSystem.IdeActions;
29 import com.intellij.openapi.application.ApplicationManager;
30 import com.intellij.openapi.command.CommandProcessor;
31 import com.intellij.openapi.diagnostic.Logger;
32 import com.intellij.openapi.editor.*;
33 import com.intellij.openapi.editor.actionSystem.EditorActionManager;
34 import com.intellij.openapi.editor.ex.util.EditorUtil;
35 import com.intellij.openapi.extensions.Extensions;
36 import com.intellij.openapi.fileEditor.FileDocumentManager;
37 import com.intellij.openapi.project.Project;
38 import com.intellij.openapi.util.Key;
39 import com.intellij.openapi.util.Segment;
40 import com.intellij.openapi.util.TextRange;
41 import com.intellij.openapi.util.UserDataHolder;
42 import com.intellij.openapi.vfs.VirtualFile;
43 import com.intellij.psi.*;
44 import com.intellij.psi.codeStyle.CodeStyleManager;
45 import com.intellij.psi.codeStyle.CodeStyleSettings;
46 import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
47 import com.intellij.psi.formatter.DocumentBasedFormattingModel;
48 import com.intellij.psi.impl.source.PostprocessReformattingAspect;
49 import com.intellij.psi.impl.source.SourceTreeToPsiMap;
50 import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
51 import com.intellij.psi.util.PsiTreeUtil;
52 import com.intellij.psi.util.PsiUtilBase;
53 import com.intellij.testFramework.LightVirtualFile;
54 import com.intellij.util.IncorrectOperationException;
55 import com.intellij.util.containers.ContainerUtilRt;
56 import com.intellij.util.text.CharArrayUtil;
57 import org.jetbrains.annotations.NonNls;
58 import org.jetbrains.annotations.NotNull;
59 import org.jetbrains.annotations.Nullable;
60
61 import java.awt.*;
62 import java.util.*;
63 import java.util.List;
64
65 public class CodeFormatterFacade {
66
67   private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.codeStyle.CodeFormatterFacade");
68
69   private static final String WRAP_LINE_COMMAND_NAME = "AutoWrapLongLine";
70
71   /**
72    * This key is used as a flag that indicates if <code>'wrap long line during formatting'</code> activity is performed now.
73    *
74    * @see CodeStyleSettings#WRAP_LONG_LINES
75    */
76   public static final Key<Boolean> WRAP_LONG_LINE_DURING_FORMATTING_IN_PROGRESS_KEY
77     = new Key<Boolean>("WRAP_LONG_LINE_DURING_FORMATTING_IN_PROGRESS_KEY");
78
79   private final CodeStyleSettings mySettings;
80   private final FormatterTagHandler myTagHandler;
81   private final int myRightMargin;
82
83   public CodeFormatterFacade(CodeStyleSettings settings, @Nullable Language language) {
84     mySettings = settings;
85     myTagHandler = new FormatterTagHandler(settings);
86     myRightMargin = mySettings.getRightMargin(language);
87   }
88
89   public ASTNode processElement(ASTNode element) {
90     TextRange range = element.getTextRange();
91     return processRange(element, range.getStartOffset(), range.getEndOffset());
92   }
93
94   public ASTNode processRange(final ASTNode element, final int startOffset, final int endOffset) {
95     return doProcessRange(element, startOffset, endOffset, null);
96   }
97
98   /**
99    * rangeMarker will be disposed
100    */
101   public ASTNode processRange(@NotNull ASTNode element, @NotNull RangeMarker rangeMarker) {
102     return doProcessRange(element, rangeMarker.getStartOffset(), rangeMarker.getEndOffset(), rangeMarker);
103   }
104
105   private ASTNode doProcessRange(final ASTNode element, final int startOffset, final int endOffset, @Nullable RangeMarker rangeMarker) {
106     final PsiElement psiElement = SourceTreeToPsiMap.treeElementToPsi(element);
107     assert psiElement != null;
108     final PsiFile file = psiElement.getContainingFile();
109     final Document document = file.getViewProvider().getDocument();
110
111     PsiElement elementToFormat = document instanceof DocumentWindow ? InjectedLanguageManager
112           .getInstance(file.getProject()).getTopLevelFile(file) : psiElement;
113     final PsiFile fileToFormat = elementToFormat.getContainingFile();
114
115     final FormattingModelBuilder builder = LanguageFormatting.INSTANCE.forContext(fileToFormat);
116     if (builder != null) {
117       if (rangeMarker == null && document != null && endOffset < document.getTextLength()) {
118         rangeMarker = document.createRangeMarker(startOffset, endOffset);
119       }
120
121       TextRange range = preprocess(element, TextRange.create(startOffset, endOffset));
122       if (document instanceof DocumentWindow) {
123         DocumentWindow documentWindow = (DocumentWindow)document;
124         range = documentWindow.injectedToHost(range);
125       }
126
127       //final SmartPsiElementPointer pointer = SmartPointerManager.getInstance(psiElement.getProject()).createSmartPsiElementPointer(psiElement);
128       final FormattingModel model = CoreFormatterUtil.buildModel(builder, elementToFormat, mySettings, FormattingMode.REFORMAT);
129       if (file.getTextLength() > 0) {
130         try {
131           FormatterEx.getInstanceEx().format(
132             model, mySettings,mySettings.getIndentOptionsByFile(fileToFormat, range), new FormatTextRanges(range, true)
133           );
134
135           wrapLongLinesIfNecessary(file, document, startOffset, endOffset);
136         }
137         catch (IncorrectOperationException e) {
138           LOG.error(e);
139         }
140       }
141
142       if (!psiElement.isValid()) {
143         if (rangeMarker != null) {
144           final PsiElement at = file.findElementAt(rangeMarker.getStartOffset());
145           final PsiElement result = PsiTreeUtil.getParentOfType(at, psiElement.getClass(), false);
146           assert result != null;
147           rangeMarker.dispose();
148           return result.getNode();
149         } else {
150           assert false;
151         }
152       }
153 //      return SourceTreeToPsiMap.psiElementToTree(pointer.getElement());
154     }
155
156     if (rangeMarker != null) {
157       rangeMarker.dispose();
158     }
159     return element;
160   }
161
162   public void processText(PsiFile file, final FormatTextRanges ranges, boolean doPostponedFormatting) {
163     final Project project = file.getProject();
164     Document document = PsiDocumentManager.getInstance(project).getDocument(file);
165     final List<FormatTextRanges.FormatTextRange> textRanges = ranges.getRanges();
166     if (document instanceof DocumentWindow) {
167       file = InjectedLanguageManager.getInstance(file.getProject()).getTopLevelFile(file);
168       final DocumentWindow documentWindow = (DocumentWindow)document;
169       for (FormatTextRanges.FormatTextRange range : textRanges) {
170         range.setTextRange(documentWindow.injectedToHost(range.getTextRange()));
171       }
172       document = documentWindow.getDelegate();
173     }
174
175
176     final FormattingModelBuilder builder = LanguageFormatting.INSTANCE.forContext(file);
177     final Language contextLanguage = file.getLanguage();
178
179     if (builder != null) {
180       if (file.getTextLength() > 0) {
181         LOG.assertTrue(document != null);
182         try {
183           final FileViewProvider viewProvider = file.getViewProvider();
184           final PsiElement startElement = viewProvider.findElementAt(textRanges.get(0).getTextRange().getStartOffset(), contextLanguage);
185           final PsiElement endElement =
186             viewProvider.findElementAt(textRanges.get(textRanges.size() - 1).getTextRange().getEndOffset() - 1, contextLanguage);
187           final PsiElement commonParent = startElement != null && endElement != null ? PsiTreeUtil.findCommonParent(startElement, endElement) : null;
188           ASTNode node = null;
189           if (commonParent != null) {
190             node = commonParent.getNode();
191           }
192           if (node == null) {
193             node = file.getNode();
194           }
195           for (FormatTextRanges.FormatTextRange range : ranges.getRanges()) {
196             TextRange rangeToUse = preprocess(node, range.getTextRange());
197             range.setTextRange(rangeToUse);
198           }
199           if (doPostponedFormatting) {
200             RangeMarker[] markers = new RangeMarker[textRanges.size()];
201             int i = 0;
202             for (FormatTextRanges.FormatTextRange range : textRanges) {
203               TextRange textRange = range.getTextRange();
204               int start = textRange.getStartOffset();
205               int end = textRange.getEndOffset();
206               if (start >= 0 && end > start && end <= document.getTextLength()) {
207                 markers[i] = document.createRangeMarker(textRange);
208                 markers[i].setGreedyToLeft(true);
209                 markers[i].setGreedyToRight(true);
210                 i++;
211               }
212             }
213             final PostprocessReformattingAspect component = file.getProject().getComponent(PostprocessReformattingAspect.class);
214             FormattingProgressTask.FORMATTING_CANCELLED_FLAG.set(false);
215             component.doPostponedFormatting(file.getViewProvider());
216             i = 0;
217             for (FormatTextRanges.FormatTextRange range : textRanges) {
218               RangeMarker marker = markers[i];
219               if (marker != null) {
220                 range.setTextRange(TextRange.create(marker));
221                 marker.dispose();
222               }
223               i++;
224             }
225           }
226           if (FormattingProgressTask.FORMATTING_CANCELLED_FLAG.get()) {
227             return;
228           }
229
230           final FormattingModel originalModel = CoreFormatterUtil.buildModel(builder, file, mySettings, FormattingMode.REFORMAT);
231           final FormattingModel model = new DocumentBasedFormattingModel(originalModel,
232                                                                          document,
233                                                                          project, mySettings, file.getFileType(), file);
234
235           FormatterEx formatter = FormatterEx.getInstanceEx();
236           if (CodeStyleManager.getInstance(project).isSequentialProcessingAllowed()) {
237             formatter.setProgressTask(new FormattingProgressTask(project, file, document));
238           }
239
240           CommonCodeStyleSettings.IndentOptions indentOptions =
241             mySettings.getIndentOptionsByFile(file, textRanges.size() == 1 ? textRanges.get(0).getTextRange() : null);
242
243           formatter.format(model, mySettings, indentOptions, ranges);
244           for (FormatTextRanges.FormatTextRange range : textRanges) {
245             TextRange textRange = range.getTextRange();
246             wrapLongLinesIfNecessary(file, document, textRange.getStartOffset(), textRange.getEndOffset());
247           }
248         }
249         catch (IncorrectOperationException e) {
250           LOG.error(e);
251         }
252       }
253     }
254   }
255
256   private TextRange preprocess(@NotNull final ASTNode node, @NotNull TextRange range) {
257     TextRange result = range;
258     PsiElement psi = node.getPsi();
259     if (!psi.isValid()) {
260       return result;
261     }
262
263     PsiFile file = psi.getContainingFile();
264
265     // We use a set here because we encountered a situation when more than one PSI leaf points to the same injected fragment
266     // (at least for sql injected into sql).
267     final LinkedHashSet<TextRange> injectedFileRangesSet = ContainerUtilRt.newLinkedHashSet();
268
269     if (!psi.getProject().isDefault()) {
270       List<DocumentWindow> injectedDocuments = InjectedLanguageUtil.getCachedInjectedDocuments(file);
271       if (!injectedDocuments.isEmpty()) {
272         for (DocumentWindow injectedDocument : injectedDocuments) {
273           injectedFileRangesSet.add(TextRange.from(injectedDocument.injectedToHost(0), injectedDocument.getTextLength()));
274         }
275       }
276       else {
277         Collection<PsiLanguageInjectionHost> injectionHosts = collectInjectionHosts(file, range);
278         PsiLanguageInjectionHost.InjectedPsiVisitor visitor = new PsiLanguageInjectionHost.InjectedPsiVisitor() {
279           @Override
280           public void visit(@NotNull PsiFile injectedPsi, @NotNull List<PsiLanguageInjectionHost.Shred> places) {
281             for (PsiLanguageInjectionHost.Shred place : places) {
282               Segment rangeMarker = place.getHostRangeMarker();
283               injectedFileRangesSet.add(TextRange.create(rangeMarker.getStartOffset(), rangeMarker.getEndOffset()));
284             }
285           }
286         };
287         for (PsiLanguageInjectionHost host : injectionHosts) {
288           InjectedLanguageUtil.enumerate(host, visitor);
289         }
290       }
291     }
292
293     if (!injectedFileRangesSet.isEmpty()) {
294       List<TextRange> ranges = ContainerUtilRt.newArrayList(injectedFileRangesSet);
295       Collections.reverse(ranges);
296       for (TextRange injectedFileRange : ranges) {
297         int startHostOffset = injectedFileRange.getStartOffset();
298         int endHostOffset = injectedFileRange.getEndOffset();
299         if (startHostOffset >= range.getStartOffset() && endHostOffset <= range.getEndOffset()) {
300           PsiFile injected = InjectedLanguageUtil.findInjectedPsiNoCommit(file, startHostOffset);
301           if (injected != null) {
302             int startInjectedOffset = range.getStartOffset() > startHostOffset ? startHostOffset - range.getStartOffset() : 0;
303             int endInjectedOffset = injected.getTextLength();
304             if (range.getEndOffset() < endHostOffset) {
305               endInjectedOffset -= endHostOffset - range.getEndOffset();
306             }
307             final TextRange initialInjectedRange = TextRange.create(startInjectedOffset, endInjectedOffset);
308             TextRange injectedRange = initialInjectedRange;
309             for (PreFormatProcessor processor : Extensions.getExtensions(PreFormatProcessor.EP_NAME)) {
310               injectedRange = processor.process(injected.getNode(), injectedRange);
311             }
312
313             // Allow only range expansion (not reduction) for injected context.
314             if ((initialInjectedRange.getStartOffset() > injectedRange.getStartOffset() && initialInjectedRange.getStartOffset() > 0)
315                 || (initialInjectedRange.getEndOffset() < injectedRange.getEndOffset()
316                     && initialInjectedRange.getEndOffset() < injected.getTextLength()))
317             {
318               range = TextRange.create(
319                 range.getStartOffset() + injectedRange.getStartOffset() - initialInjectedRange.getStartOffset(),
320                 range.getEndOffset() + initialInjectedRange.getEndOffset() - injectedRange.getEndOffset());
321             }
322           }
323         }
324       }
325     }
326
327     if (!mySettings.FORMATTER_TAGS_ENABLED) {
328       for(PreFormatProcessor processor: Extensions.getExtensions(PreFormatProcessor.EP_NAME)) {
329         result = processor.process(node, result);
330       }
331     }
332     else {
333       result = preprocessEnabledRanges(node, result);
334     }
335
336     return result;
337   }
338
339   private TextRange preprocessEnabledRanges(@NotNull final ASTNode node, @NotNull TextRange range) {
340     TextRange result = TextRange.create(range.getStartOffset(), range.getEndOffset());
341     List<TextRange> enabledRanges = myTagHandler.getEnabledRanges(node, result);
342     int delta = 0;
343     for (TextRange enabledRange : enabledRanges) {
344       enabledRange = enabledRange.shiftRight(delta);
345       for (PreFormatProcessor processor : Extensions.getExtensions(PreFormatProcessor.EP_NAME)) {
346         TextRange processedRange = processor.process(node, enabledRange);
347         delta += processedRange.getLength() - enabledRange.getLength();
348       }
349     }
350     result = result.grown(delta);
351     return result;
352   }
353
354   @NotNull
355   private static Collection<PsiLanguageInjectionHost> collectInjectionHosts(@NotNull PsiFile file, @NotNull TextRange range) {
356     Stack<PsiElement> toProcess = new Stack<PsiElement>();
357     for (PsiElement e = file.findElementAt(range.getStartOffset()); e != null; e = e.getNextSibling()) {
358       if (e.getTextRange().getStartOffset() >= range.getEndOffset()) {
359         break;
360       }
361       toProcess.push(e);
362     }
363     if (toProcess.isEmpty()) {
364       return Collections.emptySet();
365     }
366     Set<PsiLanguageInjectionHost> result = null;
367     while (!toProcess.isEmpty()) {
368       PsiElement e = toProcess.pop();
369       if (e instanceof PsiLanguageInjectionHost) {
370         if (result == null) {
371           result = ContainerUtilRt.newHashSet();
372         }
373         result.add((PsiLanguageInjectionHost)e);
374       }
375       else {
376         for (PsiElement child = e.getFirstChild(); child != null; child = child.getNextSibling()) {
377           if (e.getTextRange().getStartOffset() >= range.getEndOffset()) {
378             break;
379           }
380           toProcess.push(child);
381         }
382       }
383     }
384     return result == null ? Collections.<PsiLanguageInjectionHost>emptySet() : result;
385   }
386
387
388   /**
389    * Inspects all lines of the given document and wraps all of them that exceed {@link CodeStyleSettings#getRightMargin(com.intellij.lang.Language)}
390    * right margin}.
391    * <p/>
392    * I.e. the algorithm is to do the following for every line:
393    * <p/>
394    * <pre>
395    * <ol>
396    *   <li>
397    *      Check if the line exceeds {@link CodeStyleSettings#getRightMargin(com.intellij.lang.Language)}  right margin}. Go to the next line in the case of
398    *      negative answer;
399    *   </li>
400    *   <li>Determine line wrap position; </li>
401    *   <li>
402    *      Perform 'smart wrap', i.e. not only wrap the line but insert additional characters over than line feed if necessary.
403    *      For example consider that we wrap a single-line comment - we need to insert comment symbols on a start of the wrapped
404    *      part as well. Generally, we get the same behavior as during pressing 'Enter' at wrap position during editing document;
405    *   </li>
406    * </ol>
407    </pre>
408    *
409    * @param file        file that holds parsed document tree
410    * @param document    target document
411    * @param startOffset start offset of the first line to check for wrapping (inclusive)
412    * @param endOffset   end offset of the first line to check for wrapping (exclusive)
413    */
414   private void wrapLongLinesIfNecessary(@NotNull final PsiFile file, @Nullable final Document document, final int startOffset,
415                                         final int endOffset)
416   {
417     if (!mySettings.getCommonSettings(file.getLanguage()).WRAP_LONG_LINES ||
418         PostprocessReformattingAspect.getInstance(file.getProject()).isViewProviderLocked(file.getViewProvider()) ||
419         document == null) {
420       return;
421     }
422
423     final VirtualFile vFile = FileDocumentManager.getInstance().getFile(document);
424     if ((vFile == null || vFile instanceof LightVirtualFile) && !ApplicationManager.getApplication().isUnitTestMode()) {
425       // we assume that control flow reaches this place when the document is backed by a "virtual" file so any changes made by
426       // a formatter affect only PSI and it is out of sync with a document text
427       return;
428     }
429
430     Editor editor = PsiUtilBase.findEditor(file);
431     EditorFactory editorFactory = null;
432     if (editor == null) {
433       if (!ApplicationManager.getApplication().isDispatchThread()) {
434         return;
435       }
436       editorFactory = EditorFactory.getInstance();
437       editor = editorFactory.createEditor(document, file.getProject());
438     }
439     try {
440       final Editor editorToUse = editor;
441       ApplicationManager.getApplication().runWriteAction(new Runnable() {
442         @Override
443         public void run() {
444           final CaretModel caretModel = editorToUse.getCaretModel();
445           final int caretOffset = caretModel.getOffset();
446           final RangeMarker caretMarker = editorToUse.getDocument().createRangeMarker(caretOffset, caretOffset);
447           doWrapLongLinesIfNecessary(editorToUse, file.getProject(), editorToUse.getDocument(), startOffset, endOffset);
448           if (caretMarker.isValid() && caretModel.getOffset() != caretMarker.getStartOffset()) {
449             caretModel.moveToOffset(caretMarker.getStartOffset());
450           }
451         }
452       });
453     }
454     finally {
455       PsiDocumentManager documentManager = PsiDocumentManager.getInstance(file.getProject());
456       if (documentManager.isUncommited(document)) documentManager.commitDocument(document);
457       if (editorFactory != null) {
458         editorFactory.releaseEditor(editor);
459       }
460     }
461   }
462
463   public void doWrapLongLinesIfNecessary(@NotNull final Editor editor, @NotNull final Project project, @NotNull Document document,
464                                          int startOffset, int endOffset) {
465     // Normalization.
466     int startOffsetToUse = Math.min(document.getTextLength(), Math.max(0, startOffset));
467     int endOffsetToUse = Math.min(document.getTextLength(), Math.max(0, endOffset));
468
469     LineWrapPositionStrategy strategy = LanguageLineWrapPositionStrategy.INSTANCE.forEditor(editor);
470     CharSequence text = document.getCharsSequence();
471     int startLine = document.getLineNumber(startOffsetToUse);
472     int endLine = document.getLineNumber(Math.max(0, endOffsetToUse - 1));
473     int maxLine = Math.min(document.getLineCount(), endLine + 1);
474     int tabSize = EditorUtil.getTabSize(editor);
475     if (tabSize <= 0) {
476       tabSize = 1;
477     }
478     int spaceSize = EditorUtil.getSpaceWidth(Font.PLAIN, editor);
479     int[] shifts = new int[2];
480     // shifts[0] - lines shift.
481     // shift[1] - offset shift.
482
483     for (int line = startLine; line < maxLine; line++) {
484       int startLineOffset = document.getLineStartOffset(line);
485       int endLineOffset = document.getLineEndOffset(line);
486       final int preferredWrapPosition
487         = calculatePreferredWrapPosition(editor, text, tabSize, spaceSize, startLineOffset, endLineOffset, endOffsetToUse);
488
489       if (preferredWrapPosition < 0 || preferredWrapPosition >= endLineOffset) {
490         continue;
491       }
492       if (preferredWrapPosition >= endOffsetToUse) {
493         return;
494       }
495
496       // We know that current line exceeds right margin if control flow reaches this place, so, wrap it.
497       int wrapOffset = strategy.calculateWrapPosition(
498         document, editor.getProject(), Math.max(startLineOffset, startOffsetToUse), Math.min(endLineOffset, endOffsetToUse),
499         preferredWrapPosition, false, false
500       );
501       if (wrapOffset < 0 // No appropriate wrap position is found.
502           // No point in splitting line when its left part contains only white spaces, example:
503           //    line start -> |                   | <- right margin
504           //                  |   aaaaaaaaaaaaaaaa|aaaaaaaaaaaaaaaaaaaa() <- don't want to wrap this line even if it exceeds right margin
505           || CharArrayUtil.shiftBackward(text, startLineOffset, wrapOffset - 1, " \t") < startLineOffset) {
506         continue;
507       }
508
509       // Move caret to the target position and emulate pressing <enter>.
510       editor.getCaretModel().moveToOffset(wrapOffset);
511       emulateEnter(editor, project, shifts);
512
513       //If number of inserted symbols on new line after wrapping more or equal then symbols left on previous line
514       //there was no point to wrapping it, so reverting to before wrapping version
515       if (shifts[1] - 1 >= wrapOffset - startLineOffset) {
516         document.deleteString(wrapOffset, wrapOffset + shifts[1]);
517       }
518       else {
519         // We know that number of lines is just increased, hence, update the data accordingly.
520         maxLine += shifts[0];
521         endOffsetToUse += shifts[1];
522       }
523
524     }
525   }
526
527   /**
528    * Emulates pressing <code>Enter</code> at current caret position.
529    *
530    * @param editor       target editor
531    * @param project      target project
532    * @param shifts       two-elements array which is expected to be filled with the following info:
533    *                       1. The first element holds added lines number;
534    *                       2. The second element holds added symbols number;
535    */
536   private static void emulateEnter(@NotNull final Editor editor, @NotNull Project project, int[] shifts) {
537     final DataContext dataContext = prepareContext(editor.getComponent(), project);
538     int caretOffset = editor.getCaretModel().getOffset();
539     Document document = editor.getDocument();
540     SelectionModel selectionModel = editor.getSelectionModel();
541     int startSelectionOffset = 0;
542     int endSelectionOffset = 0;
543     boolean restoreSelection = selectionModel.hasSelection();
544     if (restoreSelection) {
545       startSelectionOffset = selectionModel.getSelectionStart();
546       endSelectionOffset = selectionModel.getSelectionEnd();
547       selectionModel.removeSelection();
548     }
549     int textLengthBeforeWrap = document.getTextLength();
550     int lineCountBeforeWrap = document.getLineCount();
551
552     DataManager.getInstance().saveInDataContext(dataContext, WRAP_LONG_LINE_DURING_FORMATTING_IN_PROGRESS_KEY, true);
553     CommandProcessor commandProcessor = CommandProcessor.getInstance();
554     try {
555       Runnable command = new Runnable() {
556         @Override
557         public void run() {
558           EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_ENTER).execute(editor, dataContext);
559         }
560       };
561       if (commandProcessor.getCurrentCommand() == null) {
562         commandProcessor.executeCommand(editor.getProject(), command, WRAP_LINE_COMMAND_NAME, null);
563       }
564       else {
565         command.run();
566       }
567     }
568     finally {
569       DataManager.getInstance().saveInDataContext(dataContext, WRAP_LONG_LINE_DURING_FORMATTING_IN_PROGRESS_KEY, null);
570     }
571     int symbolsDiff = document.getTextLength() - textLengthBeforeWrap;
572     if (restoreSelection) {
573       int newSelectionStart = startSelectionOffset;
574       int newSelectionEnd = endSelectionOffset;
575       if (startSelectionOffset >= caretOffset) {
576         newSelectionStart += symbolsDiff;
577       }
578       if (endSelectionOffset >= caretOffset) {
579         newSelectionEnd += symbolsDiff;
580       }
581       selectionModel.setSelection(newSelectionStart, newSelectionEnd);
582     }
583     shifts[0] = document.getLineCount() - lineCountBeforeWrap;
584     shifts[1] = symbolsDiff;
585   }
586
587   /**
588    * Checks if it's worth to try to wrap target line (it's long enough) and tries to calculate preferred wrap position.
589    *
590    * @param editor                target editor
591    * @param text                  text contained at the given editor
592    * @param tabSize               tab space to use (number of visual columns occupied by a tab)
593    * @param spaceSize             space width in pixels
594    * @param startLineOffset       start offset of the text line to process
595    * @param endLineOffset         end offset of the text line to process
596    * @param targetRangeEndOffset  target text region's end offset
597    * @return                      negative value if no wrapping should be performed for the target line;
598    *                              preferred wrap position otherwise
599    */
600   private int calculatePreferredWrapPosition(@NotNull Editor editor,
601                                              @NotNull CharSequence text,
602                                              int tabSize,
603                                              int spaceSize,
604                                              int startLineOffset,
605                                              int endLineOffset,
606                                              int targetRangeEndOffset) {
607     boolean hasTabs = false;
608     boolean canOptimize = true;
609     boolean hasNonSpaceSymbols = false;
610     loop:
611     for (int i = startLineOffset; i < Math.min(endLineOffset, targetRangeEndOffset); i++) {
612       char c = text.charAt(i);
613       switch (c) {
614         case '\t': {
615           hasTabs = true;
616           if (hasNonSpaceSymbols) {
617             canOptimize = false;
618             break loop;
619           }
620         }
621         case ' ': break;
622         default: hasNonSpaceSymbols = true;
623       }
624     }
625
626     if (!hasTabs) {
627       return wrapPositionForTextWithoutTabs(startLineOffset, endLineOffset, targetRangeEndOffset);
628     }
629     else if (canOptimize) {
630       return wrapPositionForTabbedTextWithOptimization(text, tabSize, startLineOffset, endLineOffset, targetRangeEndOffset);
631     }
632     else {
633       return wrapPositionForTabbedTextWithoutOptimization(editor, text, spaceSize, startLineOffset, endLineOffset, targetRangeEndOffset);
634     }
635   }
636
637   private int wrapPositionForTextWithoutTabs(int startLineOffset, int endLineOffset, int targetRangeEndOffset) {
638     if (Math.min(endLineOffset, targetRangeEndOffset) - startLineOffset > myRightMargin) {
639       return startLineOffset + myRightMargin - FormatConstants.RESERVED_LINE_WRAP_WIDTH_IN_COLUMNS;
640     }
641     return -1;
642   }
643
644   private int wrapPositionForTabbedTextWithOptimization(@NotNull CharSequence text,
645                                                         int tabSize,
646                                                         int startLineOffset,
647                                                         int endLineOffset,
648                                                         int targetRangeEndOffset)
649   {
650     int width = 0;
651     int symbolWidth;
652     int result = Integer.MAX_VALUE;
653     boolean wrapLine = false;
654     for (int i = startLineOffset; i < Math.min(endLineOffset, targetRangeEndOffset); i++) {
655       char c = text.charAt(i);
656       switch (c) {
657         case '\t': symbolWidth = tabSize - (width % tabSize); break;
658         default: symbolWidth = 1;
659       }
660       if (width + symbolWidth + FormatConstants.RESERVED_LINE_WRAP_WIDTH_IN_COLUMNS >= myRightMargin
661           && (Math.min(endLineOffset, targetRangeEndOffset) - i) >= FormatConstants.RESERVED_LINE_WRAP_WIDTH_IN_COLUMNS)
662       {
663         // Remember preferred position.
664         result = i - 1;
665       }
666       if (width + symbolWidth >= myRightMargin) {
667         wrapLine = true;
668         break;
669       }
670       width += symbolWidth;
671     }
672     return wrapLine ? result : -1;
673   }
674
675   private int wrapPositionForTabbedTextWithoutOptimization(@NotNull Editor editor,
676                                                            @NotNull CharSequence text,
677                                                            int spaceSize,
678                                                            int startLineOffset,
679                                                            int endLineOffset,
680                                                            int targetRangeEndOffset)
681   {
682     int width = 0;
683     int x = 0;
684     int newX;
685     int symbolWidth;
686     int result = Integer.MAX_VALUE;
687     boolean wrapLine = false;
688     for (int i = startLineOffset; i < Math.min(endLineOffset, targetRangeEndOffset); i++) {
689       char c = text.charAt(i);
690       switch (c) {
691         case '\t':
692           newX = EditorUtil.nextTabStop(x, editor);
693           int diffInPixels = newX - x;
694           symbolWidth = diffInPixels / spaceSize;
695           if (diffInPixels % spaceSize > 0) {
696             symbolWidth++;
697           }
698           break;
699         default: newX = x + EditorUtil.charWidth(c, Font.PLAIN, editor); symbolWidth = 1;
700       }
701       if (width + symbolWidth + FormatConstants.RESERVED_LINE_WRAP_WIDTH_IN_COLUMNS >= myRightMargin
702           && (Math.min(endLineOffset, targetRangeEndOffset) - i) >= FormatConstants.RESERVED_LINE_WRAP_WIDTH_IN_COLUMNS)
703       {
704         result = i - 1;
705       }
706       if (width + symbolWidth >= myRightMargin) {
707         wrapLine = true;
708         break;
709       }
710       x = newX;
711       width += symbolWidth;
712     }
713     return wrapLine ? result : -1;
714   }
715
716   @NotNull
717   private static DataContext prepareContext(@NotNull Component component, @NotNull final Project project) {
718     // There is a possible case that formatting is performed from project view and editor is not opened yet. The problem is that
719     // its data context doesn't contain information about project then. So, we explicitly support that here (see IDEA-72791).
720     final DataContext baseDataContext = DataManager.getInstance().getDataContext(component);
721     return new DelegatingDataContext(baseDataContext) {
722       @Override
723       public Object getData(@NonNls String dataId) {
724         Object result = baseDataContext.getData(dataId);
725         if (result == null && CommonDataKeys.PROJECT.is(dataId)) {
726           result = project;
727         }
728         return result;
729       }
730     };
731   }
732
733   private static class DelegatingDataContext implements DataContext, UserDataHolder {
734
735     private final DataContext myDataContextDelegate;
736     private final UserDataHolder myDataHolderDelegate;
737
738     DelegatingDataContext(DataContext delegate) {
739       myDataContextDelegate = delegate;
740       if (delegate instanceof UserDataHolder) {
741         myDataHolderDelegate = (UserDataHolder)delegate;
742       }
743       else {
744         myDataHolderDelegate = null;
745       }
746     }
747
748     @Override
749     public Object getData(@NonNls String dataId) {
750       return myDataContextDelegate.getData(dataId);
751     }
752
753     @Override
754     public <T> T getUserData(@NotNull Key<T> key) {
755       return myDataHolderDelegate == null ? null : myDataHolderDelegate.getUserData(key);
756     }
757
758     @Override
759     public <T> void putUserData(@NotNull Key<T> key, @Nullable T value) {
760       if (myDataHolderDelegate != null) {
761         myDataHolderDelegate.putUserData(key, value);
762       }
763     }
764   }
765 }
766