CPP-3103: Conditionally uncompiled code unexpectedly formatted
[idea/community.git] / platform / lang-impl / src / com / intellij / psi / formatter / DocumentBasedFormattingModel.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.formatter;
18
19 import com.intellij.formatting.Block;
20 import com.intellij.formatting.FormattingDocumentModel;
21 import com.intellij.formatting.FormattingModel;
22 import com.intellij.formatting.FormattingModelEx;
23 import com.intellij.formatting.FormattingModelWithShiftIndentInsideDocumentRange;
24 import com.intellij.lang.ASTNode;
25 import com.intellij.openapi.editor.Document;
26 import com.intellij.openapi.fileTypes.FileType;
27 import com.intellij.openapi.project.Project;
28 import com.intellij.openapi.util.TextRange;
29 import com.intellij.openapi.util.text.StringUtil;
30 import com.intellij.psi.PsiDocumentManager;
31 import com.intellij.psi.PsiFile;
32 import com.intellij.psi.codeStyle.CodeStyleSettings;
33 import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
34 import com.intellij.psi.impl.source.codeStyle.CodeEditUtil;
35 import com.intellij.util.text.CharArrayUtil;
36 import org.jetbrains.annotations.NotNull;
37 import org.jetbrains.annotations.Nullable;
38
39 /**
40  * @author lesya
41  */
42 public class DocumentBasedFormattingModel implements FormattingModelEx {
43   private final Block                   myRootBlock;
44   private final FormattingDocumentModel myDocumentModel;
45   @Nullable private final FormattingModel myOriginalFormattingModel;
46   @NotNull private final Document       myDocument;
47   private final Project                 myProject;
48   private final CodeStyleSettings       mySettings;
49   private final FileType                myFileType;
50   private final PsiFile                 myFile;
51
52   @Deprecated
53   public DocumentBasedFormattingModel(final Block rootBlock,
54                                       @NotNull final Document document,
55                                       final Project project,
56                                       final CodeStyleSettings settings,
57                                       final FileType fileType,
58                                       final PsiFile file) {
59     myRootBlock = rootBlock;
60     myDocument = document;
61     myProject = project;
62     mySettings = settings;
63     myFileType = fileType;
64     myFile = file;
65     myDocumentModel = new FormattingDocumentModelImpl(document,file);
66     myOriginalFormattingModel = null;
67   }
68
69   public DocumentBasedFormattingModel(final Block rootBlock,
70                                       final Project project,
71                                       final CodeStyleSettings settings,
72                                       final FileType fileType,
73                                       final PsiFile file) {
74     myRootBlock = rootBlock;
75     myProject = project;
76     mySettings = settings;
77     myFileType = fileType;
78     myFile = file;
79     myDocumentModel = FormattingDocumentModelImpl.createOn(file);
80     myDocument = myDocumentModel.getDocument();
81     myOriginalFormattingModel = null;
82   }
83
84   public DocumentBasedFormattingModel(@NotNull final FormattingModel originalModel,
85                                       @NotNull final Document document,
86                                       final Project project,
87                                       final CodeStyleSettings settings,
88                                       final FileType fileType,
89                                       final PsiFile file) {
90     myOriginalFormattingModel = originalModel;
91     myRootBlock = originalModel.getRootBlock();
92     myDocument = document;
93     myProject = project;
94     mySettings = settings;
95     myFileType = fileType;
96     myFile = file;
97     myDocumentModel = new FormattingDocumentModelImpl(document,file);
98   }
99
100   @Override
101   @NotNull
102   public Block getRootBlock() {
103     return myRootBlock;
104   }
105
106   @Override
107   @NotNull
108   public FormattingDocumentModel getDocumentModel() {
109     return myDocumentModel;
110   }
111
112
113   @Override
114   public TextRange replaceWhiteSpace(TextRange textRange, String whiteSpace) {
115     return replaceWhiteSpace(textRange, null, whiteSpace);
116   }
117
118   @Override
119   public TextRange replaceWhiteSpace(TextRange textRange, ASTNode nodeAfter, String whiteSpace) {
120     if (myOriginalFormattingModel instanceof FormattingModelWithShiftIndentInsideDocumentRange) {
121       whiteSpace = ((FormattingModelWithShiftIndentInsideDocumentRange)myOriginalFormattingModel).adjustWhiteSpaceInsideDocument(
122         nodeAfter,
123         whiteSpace);
124     }
125
126     boolean removesStartMarker;
127     String marker;
128
129     // When processing injection in cdata / comment we need not remove start / end markers that present as whitespace during check in
130     // com.intellij.formatting.WhiteSpace and during building formatter model = blocks in e.g. com.intellij.psi.formatter.xml.XmlTagBlock
131     if ((removesStartMarker = removesPattern(textRange, whiteSpace, marker = "<![CDATA[") ||
132         removesPattern(textRange, whiteSpace, marker ="<!--[")) ||
133         removesPattern(textRange, whiteSpace, marker = "]]>") ||
134         removesPattern(textRange, whiteSpace, marker = "]-->")
135       ) {
136       String newWs = null;
137
138       if (removesStartMarker) {    // TODO once we reformat comments we will need to handle their markers as well
139         int at = CharArrayUtil.indexOf(myDocument.getCharsSequence(), marker, textRange.getStartOffset(), textRange.getEndOffset() + 1);
140         String ws = myDocument.getCharsSequence().subSequence(textRange.getStartOffset(), textRange.getEndOffset()).toString();
141         newWs = mergeWsWithCdataMarker(whiteSpace, ws, at - textRange.getStartOffset());
142
143         if (removesPattern(textRange, newWs != null ? newWs: whiteSpace, marker = "]]>")) {
144           int i;
145           if (newWs != null && (i = newWs.lastIndexOf('\n')) > 0) {
146             int cdataStart = newWs.indexOf("<![CDATA[");
147             int i2 = newWs.lastIndexOf('\n', cdataStart);
148             String cdataIndent = i2 != -1 ? newWs.substring(i2 + 1, cdataStart):"";
149             newWs = newWs.substring(0, i) + cdataIndent + marker + newWs.substring(i);
150           }
151         }
152       }
153
154       if (newWs == null) return textRange;
155       whiteSpace = newWs;
156     }
157
158     CharSequence whiteSpaceToUse = getDocumentModel().adjustWhiteSpaceIfNecessary(
159       whiteSpace, textRange.getStartOffset(), textRange.getEndOffset(), nodeAfter, false
160     );
161
162     myDocument.replaceString(textRange.getStartOffset(),
163                            textRange.getEndOffset(),
164                            whiteSpaceToUse);
165
166     return new TextRange(textRange.getStartOffset(), textRange.getStartOffset() + whiteSpaceToUse.length());
167   }
168
169   private boolean removesPattern(final TextRange textRange, final String whiteSpace, final String pattern) {
170     return CharArrayUtil.indexOf(myDocument.getCharsSequence(), pattern, textRange.getStartOffset(), textRange.getEndOffset() + 1) >= 0 &&
171         CharArrayUtil.indexOf(whiteSpace, pattern, 0) < 0;
172   }
173
174   @Override
175   public TextRange shiftIndentInsideRange(ASTNode node, TextRange range, int indent) {
176     if (myOriginalFormattingModel instanceof FormattingModelWithShiftIndentInsideDocumentRange) {
177       final TextRange newRange =
178         ((FormattingModelWithShiftIndentInsideDocumentRange)myOriginalFormattingModel).shiftIndentInsideDocumentRange(myDocument, node, range, indent);
179       if (newRange != null)
180         return newRange;
181     }
182
183     final int newLength = shiftIndentInside(range, indent);
184     return new TextRange(range.getStartOffset(), range.getStartOffset() + newLength);
185   }
186
187   @Override
188   public void commitChanges() {
189     CodeEditUtil.allowToMarkNodesForPostponedFormatting(false);
190     try {
191       PsiDocumentManager.getInstance(myProject).commitDocument(myDocument);
192     }
193     finally {
194       CodeEditUtil.allowToMarkNodesForPostponedFormatting(true);
195     }
196   }
197
198   private int shiftIndentInside(final TextRange elementRange, final int shift) {
199     final StringBuilder buffer = new StringBuilder();
200     StringBuilder afterWhiteSpace = new StringBuilder();
201     int whiteSpaceLength = 0;
202     boolean insideWhiteSpace = true;
203     int line = 0;
204     for (int i = elementRange.getStartOffset(); i < elementRange.getEndOffset(); i++) {
205       final char c = myDocument.getCharsSequence().charAt(i);
206       switch (c) {
207         case '\n':
208           if (line > 0) {
209             createWhiteSpace(whiteSpaceLength + shift, buffer);
210           }
211           buffer.append(afterWhiteSpace.toString());
212           insideWhiteSpace = true;
213           whiteSpaceLength = 0;
214           afterWhiteSpace = new StringBuilder();
215           buffer.append(c);
216           line++;
217           break;
218         case ' ':
219           if (insideWhiteSpace) {
220             whiteSpaceLength += 1;
221           }
222           else {
223             afterWhiteSpace.append(c);
224           }
225           break;
226         case '\t':
227           if (insideWhiteSpace) {
228             whiteSpaceLength += getIndentOptions().TAB_SIZE;
229           }
230           else {
231             afterWhiteSpace.append(c);
232           }
233
234           break;
235         default:
236           insideWhiteSpace = false;
237           afterWhiteSpace.append(c);
238       }
239     }
240     if (line > 0) {
241       createWhiteSpace(whiteSpaceLength + shift, buffer);
242     }
243     buffer.append(afterWhiteSpace.toString());
244     myDocument.replaceString(elementRange.getStartOffset(), elementRange.getEndOffset(), buffer.toString());
245     return buffer.length();
246   }
247
248   private void createWhiteSpace(final int whiteSpaceLength, StringBuilder buffer) {
249     if (whiteSpaceLength < 0) return;
250     final CommonCodeStyleSettings.IndentOptions indentOptions = getIndentOptions();
251     if (indentOptions.USE_TAB_CHARACTER) {
252       int tabs = whiteSpaceLength / indentOptions.TAB_SIZE;
253       int spaces = whiteSpaceLength - tabs * indentOptions.TAB_SIZE;
254       StringUtil.repeatSymbol(buffer, '\t', tabs);
255       StringUtil.repeatSymbol(buffer, ' ', spaces);
256     }
257     else {
258       StringUtil.repeatSymbol(buffer, ' ', whiteSpaceLength);
259     }
260   }
261
262   private CommonCodeStyleSettings.IndentOptions getIndentOptions() {
263     return mySettings.getIndentOptions(myFileType);
264   }
265
266   @NotNull
267   public Document getDocument() {
268     return myDocument;
269   }
270
271   public Project getProject() {
272     return myProject;
273   }
274
275   public PsiFile getFile() {
276     return myFile;
277   }
278
279   @Nullable
280   public static String mergeWsWithCdataMarker(String whiteSpace, final String s, final int cdataPos) {
281     final int firstCrInGeneratedWs = whiteSpace.indexOf('\n');
282     final int secondCrInGeneratedWs = firstCrInGeneratedWs != -1 ? whiteSpace.indexOf('\n', firstCrInGeneratedWs + 1) : -1;
283     final int firstCrInPreviousWs = s.indexOf('\n');
284     final int secondCrInPreviousWs = firstCrInPreviousWs != -1 ? s.indexOf('\n', firstCrInPreviousWs + 1) : -1;
285
286     boolean knowHowToModifyCData = false;
287
288     if (secondCrInPreviousWs != -1 && secondCrInGeneratedWs != -1 && cdataPos > firstCrInPreviousWs && cdataPos < secondCrInPreviousWs) {
289       whiteSpace = whiteSpace.substring(0, secondCrInGeneratedWs) +
290                    s.substring(firstCrInPreviousWs + 1, secondCrInPreviousWs) +
291                    whiteSpace.substring(secondCrInGeneratedWs);
292       knowHowToModifyCData = true;
293     }
294     if (!knowHowToModifyCData) whiteSpace = null;
295     return whiteSpace;
296   }
297 }