929a0c2b757c7452c585b96f57ad7f620087f969
[idea/community.git] / platform / lang-impl / src / com / intellij / psi / impl / source / codeStyle / CodeFormatterFacade.java
1 /*
2  * Copyright 2000-2009 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.LanguageFormatting;
24 import com.intellij.openapi.actionSystem.DataContext;
25 import com.intellij.openapi.actionSystem.IdeActions;
26 import com.intellij.openapi.diagnostic.Logger;
27 import com.intellij.openapi.editor.*;
28 import com.intellij.openapi.editor.actionSystem.EditorActionManager;
29 import com.intellij.openapi.editor.ex.util.EditorUtil;
30 import com.intellij.openapi.extensions.Extensions;
31 import com.intellij.openapi.project.Project;
32 import com.intellij.openapi.util.TextRange;
33 import com.intellij.psi.PsiDocumentManager;
34 import com.intellij.psi.PsiElement;
35 import com.intellij.psi.PsiFile;
36 import com.intellij.psi.codeStyle.CodeStyleSettings;
37 import com.intellij.psi.formatter.DocumentBasedFormattingModel;
38 import com.intellij.psi.impl.source.PostprocessReformattingAspect;
39 import com.intellij.psi.impl.source.SourceTreeToPsiMap;
40 import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
41 import com.intellij.psi.util.PsiTreeUtil;
42 import com.intellij.psi.util.PsiUtilBase;
43 import com.intellij.util.IncorrectOperationException;
44 import org.jetbrains.annotations.NotNull;
45
46 import java.awt.*;
47
48 public class CodeFormatterFacade {
49   private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.codeStyle.CodeFormatterFacade");
50
51   private final CodeStyleSettings mySettings;
52
53   public CodeFormatterFacade(CodeStyleSettings settings) {
54     mySettings = settings;
55   }
56
57   public ASTNode processElement(ASTNode element) {
58     TextRange range = element.getTextRange();
59     return processRange(element, range.getStartOffset(), range.getEndOffset());
60   }
61
62   public ASTNode processRange(final ASTNode element, final int startOffset, final int endOffset) {
63     final PsiElement psiElement = SourceTreeToPsiMap.treeElementToPsi(element);
64     final PsiFile file = psiElement.getContainingFile();
65     final Document document = file.getViewProvider().getDocument();
66     final RangeMarker rangeMarker = document != null && endOffset < document.getTextLength()? document.createRangeMarker(startOffset, endOffset):null;
67
68     PsiElement elementToFormat = document instanceof DocumentWindow ? InjectedLanguageUtil.getTopLevelFile(file) : psiElement;
69     final PsiFile fileToFormat = elementToFormat.getContainingFile();
70
71     final FormattingModelBuilder builder = LanguageFormatting.INSTANCE.forContext(fileToFormat);
72     if (builder != null) {
73       TextRange range = preprocess(element, startOffset, endOffset);
74       if (document instanceof DocumentWindow) {
75         DocumentWindow documentWindow = (DocumentWindow)document;
76         range = documentWindow.injectedToHost(range);
77       }
78
79       //final SmartPsiElementPointer pointer = SmartPointerManager.getInstance(psiElement.getProject()).createSmartPsiElementPointer(psiElement);
80       final FormattingModel model = builder.createModel(elementToFormat, mySettings);
81       if (file.getTextLength() > 0) {
82         try {
83           FormatterEx.getInstanceEx().format(
84             model, mySettings,mySettings.getIndentOptions(fileToFormat.getFileType()), new FormatTextRanges(range, true)
85           );
86
87           wrapLongLinesIfNecessary(file, document, startOffset, endOffset);
88         }
89         catch (IncorrectOperationException e) {
90           LOG.error(e);
91         }
92       }
93
94       if (!psiElement.isValid()) {
95         if (rangeMarker != null) {
96           final PsiElement at = file.findElementAt(rangeMarker.getStartOffset());
97           final PsiElement result = PsiTreeUtil.getParentOfType(at, psiElement.getClass(), false);
98           assert result != null;
99           return result.getNode();
100         } else {
101           assert false;
102         }
103       }
104
105 //      return SourceTreeToPsiMap.psiElementToTree(pointer.getElement());
106
107     }
108
109     return element;
110   }
111
112   public void processText(PsiFile file, final FormatTextRanges ranges, boolean doPostponedFormatting) {
113     final Project project = file.getProject();
114     Document document = PsiDocumentManager.getInstance(project).getDocument(file);
115     if (document instanceof DocumentWindow) {
116       file = InjectedLanguageUtil.getTopLevelFile(file);
117       final DocumentWindow documentWindow = (DocumentWindow)document;
118       for (FormatTextRanges.FormatTextRange range : ranges.getRanges()) {
119         range.setTextRange(documentWindow.injectedToHost(range.getTextRange()));
120       }
121       document = documentWindow.getDelegate();
122     }
123
124
125     final FormattingModelBuilder builder = LanguageFormatting.INSTANCE.forContext(file);
126
127     if (builder != null) {
128       if (file.getTextLength() > 0) {
129         try {
130           ranges.preprocess(file.getNode());
131           if (doPostponedFormatting) {
132             RangeMarker[] markers = new RangeMarker[ranges.getRanges().size()];
133             int i = 0;
134             for (FormatTextRanges.FormatTextRange range : ranges.getRanges()) {
135               TextRange textRange = range.getTextRange();
136               int start = textRange.getStartOffset();
137               int end = textRange.getEndOffset();
138               if (start >= 0 && end > start && end <= document.getTextLength()) {
139                 markers[i] = document.createRangeMarker(textRange);
140                 markers[i].setGreedyToLeft(true);
141                 markers[i].setGreedyToRight(true);
142                 i++;
143               }
144             }
145             final PostprocessReformattingAspect component = file.getProject().getComponent(PostprocessReformattingAspect.class);
146             component.doPostponedFormatting(file.getViewProvider());
147             i = 0;
148             for (FormatTextRanges.FormatTextRange range : ranges.getRanges()) {
149               if (markers[i] != null) {
150                 range.setTextRange(new TextRange(markers[i].getStartOffset(), markers[i].getEndOffset()));
151               }
152               i++;
153             }
154           }
155           final FormattingModel originalModel = builder.createModel(file, mySettings);
156           final FormattingModel model = new DocumentBasedFormattingModel(originalModel.getRootBlock(),
157                                                                          document,
158                                                                          project, mySettings, file.getFileType(), file);
159
160           FormatterEx.getInstanceEx().format(model, mySettings, mySettings.getIndentOptions(file.getFileType()), ranges);
161           for (FormatTextRanges.FormatTextRange range : ranges.getRanges()) {
162             TextRange textRange = range.getTextRange();
163             wrapLongLinesIfNecessary(file, document, textRange.getStartOffset(), textRange.getEndOffset());
164           }
165         }
166         catch (IncorrectOperationException e) {
167           LOG.error(e);
168         }
169       }
170     }
171   }
172
173   private static TextRange preprocess(final ASTNode node, final int startOffset, final int endOffset) {
174     TextRange result = new TextRange(startOffset, endOffset);
175     for(PreFormatProcessor processor: Extensions.getExtensions(PreFormatProcessor.EP_NAME)) {
176       result = processor.process(node, result);
177     }
178     return result;
179   }
180
181   /**
182    * Inspects all lines of the given document and wraps all of them that exceed {@link CodeStyleSettings#RIGHT_MARGIN right margin}.
183    * <p/>
184    * I.e. the algorithm is to do the following for every line:
185    * <p/>
186    * <pre>
187    * <ol>
188    *   <li>
189    *      Check if the line exceeds {@link CodeStyleSettings#RIGHT_MARGIN right margin}. Go to the next line in the case of
190    *      negative answer;
191    *   </li>
192    *   <li>Determine line wrap position; </li>
193    *   <li>
194    *      Perform 'smart wrap', i.e. not only wrap the line but insert additional characters over than line feed if necessary.
195    *      For example consider that we wrap a single-line comment - we need to insert comment symbols on a start of the wrapped
196    *      part as well. Generally, we get the same behavior as during pressing 'Enter' at wrap position during editing document;
197    *   </li>
198    * </ol>
199    </pre>
200    *
201    * @param file        file that holds parsed document tree
202    * @param document    target document
203    * @param startOffset start offset of the first line to check for wrapping (inclusive)
204    * @param endOffset   end offset of the first line to check for wrapping (exclusive)
205    */
206   private void wrapLongLinesIfNecessary(@NotNull PsiFile file, @NotNull Document document, int startOffset, int endOffset) {
207     Editor editor = PsiUtilBase.findEditor(file);
208     if (editor == null) {
209       return;
210     }
211
212     LineWrapPositionStrategy strategy = LanguageLineWrapPositionStrategy.INSTANCE.forEditor(editor);
213     CharSequence text = document.getCharsSequence();
214     int startLine = document.getLineNumber(startOffset);
215     int endLine = document.getLineNumber(Math.min(document.getTextLength(), endOffset) - 1);
216     int maxLine = Math.min(document.getLineCount(), endLine + 1);
217     int tabSize = EditorUtil.getTabSize(editor);
218     int spaceSize = EditorUtil.getSpaceWidth(Font.PLAIN, editor);
219
220     for (int line = startLine; line < maxLine; line++) {
221       int startLineOffset = document.getLineStartOffset(line);
222       int endLineOffset = document.getLineEndOffset(line);
223
224       boolean hasTabs = false;
225       boolean canOptimize = true;
226       boolean hasNonSpaceSymbols = false;
227       loop:
228       for (int i = startLineOffset; i < Math.min(endLineOffset, endOffset); i++) {
229         char c = text.charAt(i);
230         switch (c) {
231           case '\t': {
232             hasTabs = true;
233             if (hasNonSpaceSymbols) {
234               canOptimize = false;
235               break loop;
236             }
237           }
238           case ' ': break;
239           default: hasNonSpaceSymbols = true;
240         }
241       }
242
243       int preferredWrapPosition = Integer.MAX_VALUE;
244       if (!hasTabs) {
245         if (Math.min(endLineOffset, endOffset) >= mySettings.RIGHT_MARGIN) {
246           preferredWrapPosition = startLineOffset + mySettings.RIGHT_MARGIN - FormatConstants.RESERVED_LINE_WRAP_WIDTH_IN_COLUMNS;
247         }
248       }
249       else if (canOptimize) {
250         int width = 0;
251         int symbolWidth;
252         for (int i = startLineOffset; i < Math.min(endLineOffset, endOffset); i++) {
253           char c = text.charAt(i);
254           switch (c) {
255             case '\t': symbolWidth = tabSize - (width % tabSize); break;
256             default: symbolWidth = 1;
257           }
258           if (width + symbolWidth + FormatConstants.RESERVED_LINE_WRAP_WIDTH_IN_COLUMNS >= mySettings.RIGHT_MARGIN
259               && (Math.min(endLineOffset, endOffset) - i) >= FormatConstants.RESERVED_LINE_WRAP_WIDTH_IN_COLUMNS)
260           {
261             preferredWrapPosition = i - 1;
262             break;
263           }
264           width += symbolWidth;
265         }
266       }
267       else {
268         int width = 0;
269         int x = 0;
270         int newX;
271         int symbolWidth;
272         for (int i = startLineOffset; i < Math.min(endLineOffset, endOffset); i++) {
273           char c = text.charAt(i);
274           switch (c) {
275             case '\t':
276               newX = EditorUtil.nextTabStop(x, editor);
277               int diffInPixels = newX - x;
278               symbolWidth = diffInPixels / spaceSize;
279               if (diffInPixels % spaceSize > 0) {
280                 symbolWidth++;
281               }
282               break;
283             default: newX = x + EditorUtil.charWidth(c, Font.PLAIN, editor); symbolWidth = 1;
284           }
285           if (width + symbolWidth + FormatConstants.RESERVED_LINE_WRAP_WIDTH_IN_COLUMNS >= mySettings.RIGHT_MARGIN
286               && (Math.min(endLineOffset, endOffset) - i) >= FormatConstants.RESERVED_LINE_WRAP_WIDTH_IN_COLUMNS)
287           {
288             preferredWrapPosition = i - 1;
289             break;
290           }
291           x = newX;
292           width += symbolWidth;
293         }
294       }
295       if (preferredWrapPosition >= endLineOffset) {
296         continue;
297       }
298       if (preferredWrapPosition >= endOffset) {
299         return;
300       }
301
302       // We know that current line exceeds right margin if control flow reaches this place, so, wrap it.
303       int wrapOffset = strategy.calculateWrapPosition(
304         text, Math.max(startLineOffset, startOffset), Math.min(endLineOffset, endOffset), preferredWrapPosition, false
305       );
306       editor.getCaretModel().moveToOffset(wrapOffset);
307       DataContext dataContext = DataManager.getInstance().getDataContext(editor.getComponent());
308
309       SelectionModel selectionModel = editor.getSelectionModel();
310       boolean restoreSelection;
311       int startSelectionOffset = 0;
312       int endSelectionOffset = 0;
313       if (restoreSelection = selectionModel.hasSelection()) {
314         startSelectionOffset = selectionModel.getSelectionStart();
315         endSelectionOffset = selectionModel.getSelectionEnd();
316         selectionModel.removeSelection();
317       }
318       int textLengthBeforeWrap = document.getTextLength();
319       EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_ENTER).execute(editor, dataContext);
320       if (restoreSelection) {
321         int symbolsDiff = document.getTextLength() - textLengthBeforeWrap;
322         int newSelectionStart = startSelectionOffset;
323         int newSelectionEnd = endSelectionOffset;
324         if (startSelectionOffset >= wrapOffset) {
325           newSelectionStart += symbolsDiff;
326         }
327         if (endSelectionOffset >= wrapOffset) {
328           newSelectionEnd += symbolsDiff;
329         }
330         selectionModel.setSelection(newSelectionStart, newSelectionEnd);
331       }
332
333
334       // There is a possible case that particular line is so long, that its part that exceeds right margin and is wrapped
335       // still exceeds right margin. Hence, we recursively call 'wrap long lines' sub-routine in order to handle that.
336
337       wrapLongLinesIfNecessary(file, document, document.getLineStartOffset(line + 1), endOffset);
338       return;
339     }
340   }
341 }
342