fix #774 infinite loop in net.sourceforge.phpeclipse.builder.IdentifierIndexManager...
[phpeclipse.git] / net.sourceforge.phpeclipse / src / net / sourceforge / phpdt / internal / corext / template / php / JavaFormatter.java
1 /*******************************************************************************
2  * Copyright (c) 2000, 2004 IBM Corporation and others.
3  * All rights reserved. This program and the accompanying materials
4  * are made available under the terms of the Common Public License v1.0
5  * which accompanies this distribution, and is available at
6  * http://www.eclipse.org/legal/cpl-v10.html
7  *
8  * Contributors:
9  *     IBM Corporation - initial API and implementation
10  *******************************************************************************/
11 package net.sourceforge.phpdt.internal.corext.template.php;
12
13 import java.util.ArrayList;
14 import java.util.Iterator;
15 import java.util.List;
16
17 import net.sourceforge.phpdt.internal.corext.util.CodeFormatterUtil;
18 import net.sourceforge.phpdt.internal.corext.util.Strings;
19 import net.sourceforge.phpdt.internal.ui.text.IPHPPartitions;
20 import net.sourceforge.phpdt.internal.ui.text.JavaHeuristicScanner;
21 import net.sourceforge.phpdt.internal.ui.text.JavaIndenter;
22 import net.sourceforge.phpeclipse.PHPeclipsePlugin;
23
24 import org.eclipse.jface.text.BadLocationException;
25 import org.eclipse.jface.text.Document;
26 import org.eclipse.jface.text.IDocument;
27 import org.eclipse.jface.text.IRegion;
28 import org.eclipse.jface.text.ITypedRegion;
29 import org.eclipse.jface.text.templates.DocumentTemplateContext;
30 import org.eclipse.jface.text.templates.GlobalTemplateVariables;
31 import org.eclipse.jface.text.templates.TemplateBuffer;
32 import org.eclipse.jface.text.templates.TemplateContext;
33 import org.eclipse.jface.text.templates.TemplateVariable;
34 import org.eclipse.text.edits.DeleteEdit;
35 import org.eclipse.text.edits.InsertEdit;
36 import org.eclipse.text.edits.MalformedTreeException;
37 import org.eclipse.text.edits.MultiTextEdit;
38 import org.eclipse.text.edits.RangeMarker;
39 import org.eclipse.text.edits.ReplaceEdit;
40 import org.eclipse.text.edits.TextEdit;
41
42 /**
43  * A template editor using the Java formatter to format a template buffer.
44  */
45 public class JavaFormatter {
46
47         private static final String MARKER = "/*${" + GlobalTemplateVariables.Cursor.NAME + "}*/"; //$NON-NLS-1$ //$NON-NLS-2$
48
49         /** The line delimiter to use if code formatter is not used. */
50         private final String fLineDelimiter;
51
52         /** The initial indent level */
53         private final int fInitialIndentLevel;
54
55         /** The java partitioner */
56         private boolean fUseCodeFormatter;
57
58         /**
59          * Creates a JavaFormatter with the target line delimiter.
60          * 
61          * @param lineDelimiter
62          *            the line delimiter to use
63          * @param initialIndentLevel
64          *            the initial indentation level
65          * @param useCodeFormatter
66          *            <code>true</code> if the core code formatter should be used
67          */
68         public JavaFormatter(String lineDelimiter, int initialIndentLevel,
69                         boolean useCodeFormatter) {
70                 fLineDelimiter = lineDelimiter;
71                 fUseCodeFormatter = useCodeFormatter;
72                 fInitialIndentLevel = initialIndentLevel;
73         }
74
75         /**
76          * Formats the template buffer.
77          * 
78          * @param buffer
79          * @param context
80          * @throws BadLocationException
81          */
82         public void format(TemplateBuffer buffer, TemplateContext context)
83                         throws BadLocationException {
84                 try {
85                         if (fUseCodeFormatter)
86                                 // try to format and fall back to indenting
87                                 try {
88                                         format(buffer, (JavaContext) context);
89                                 } catch (BadLocationException e) {
90                                         indent(buffer);
91                                 } catch (MalformedTreeException e) {
92                                         indent(buffer);
93                                 }
94                         else
95                                 indent(buffer);
96
97                         // don't trim the buffer if the replacement area is empty
98                         // case: surrounding empty lines with block
99                         if (context instanceof DocumentTemplateContext) {
100                                 DocumentTemplateContext dtc = (DocumentTemplateContext) context;
101                                 if (dtc.getStart() == dtc.getCompletionOffset())
102                                         if (dtc.getDocument().get(dtc.getStart(),
103                                                         dtc.getEnd() - dtc.getEnd()).trim().length() == 0)
104                                                 return;
105                         }
106
107                         trimBegin(buffer);
108                 } catch (MalformedTreeException e) {
109                         throw new BadLocationException();
110                 }
111         }
112
113         private static int getCaretOffset(TemplateVariable[] variables) {
114                 for (int i = 0; i != variables.length; i++) {
115                         TemplateVariable variable = variables[i];
116
117                         if (variable.getType().equals(GlobalTemplateVariables.Cursor.NAME))
118                                 return variable.getOffsets()[0];
119                 }
120
121                 return -1;
122         }
123
124         private boolean isInsideCommentOrString(String string, int offset) {
125
126                 IDocument document = new Document(string);
127                 PHPeclipsePlugin.getDefault().getJavaTextTools()
128                                 .setupJavaDocumentPartitioner(document);
129
130                 try {
131                         ITypedRegion partition = document.getPartition(offset);
132                         String partitionType = partition.getType();
133
134                         return partitionType != null
135                                         && (partitionType
136                                                         .equals(IPHPPartitions.PHP_MULTILINE_COMMENT)
137                                                         || partitionType
138                                                                         .equals(IPHPPartitions.PHP_SINGLELINE_COMMENT)
139                                                         || partitionType
140                                                                         .equals(IPHPPartitions.PHP_STRING_DQ)
141                                                         || partitionType
142                                                                         .equals(IPHPPartitions.PHP_STRING_SQ)
143                                                         || partitionType
144                                                                         .equals(IPHPPartitions.PHP_STRING_HEREDOC) || partitionType
145                                                         .equals(IPHPPartitions.PHP_PHPDOC_COMMENT));
146
147                 } catch (BadLocationException e) {
148                         return false;
149                 }
150         }
151
152         private void format(TemplateBuffer templateBuffer, JavaContext context)
153                         throws BadLocationException {
154                 // XXX 4360, 15247
155                 // workaround for code formatter limitations
156                 // handle a special case where cursor position is surrounded by
157                 // whitespaces
158
159                 String string = templateBuffer.getString();
160                 TemplateVariable[] variables = templateBuffer.getVariables();
161
162                 int caretOffset = getCaretOffset(variables);
163                 if ((caretOffset > 0)
164                                 && Character.isWhitespace(string.charAt(caretOffset - 1))
165                                 && (caretOffset < string.length())
166                                 && Character.isWhitespace(string.charAt(caretOffset))
167                                 && !isInsideCommentOrString(string, caretOffset)) {
168                         List positions = variablesToPositions(variables);
169
170                         TextEdit insert = new InsertEdit(caretOffset, MARKER);
171                         string = edit(string, positions, insert);
172                         positionsToVariables(positions, variables);
173                         templateBuffer.setContent(string, variables);
174
175                         plainFormat(templateBuffer, context);
176
177                         string = templateBuffer.getString();
178                         variables = templateBuffer.getVariables();
179                         caretOffset = getCaretOffset(variables);
180
181                         positions = variablesToPositions(variables);
182                         TextEdit delete = new DeleteEdit(caretOffset, MARKER.length());
183                         string = edit(string, positions, delete);
184                         positionsToVariables(positions, variables);
185                         templateBuffer.setContent(string, variables);
186
187                 } else {
188                         plainFormat(templateBuffer, context);
189                 }
190         }
191
192         private void plainFormat(TemplateBuffer templateBuffer, JavaContext context)
193                         throws BadLocationException {
194         }
195
196         // private void plainFormat(TemplateBuffer templateBuffer, JavaContext
197         // context) throws BadLocationException {
198         //
199         // IDocument doc= new Document(templateBuffer.getString());
200         //
201         // TemplateVariable[] variables= templateBuffer.getVariables();
202         //
203         // List offsets= variablesToPositions(variables);
204         //
205         // Map options;
206         // if (context.getCompilationUnit() != null)
207         // options= context.getCompilationUnit().getJavaProject().getOptions(true);
208         // else
209         // options= JavaCore.getOptions();
210         //
211         // TextEdit edit= CodeFormatterUtil.format2(CodeFormatter.K_UNKNOWN,
212         // doc.get(), fInitialIndentLevel, fLineDelimiter, options);
213         // if (edit == null)
214         // throw new BadLocationException(); // fall back to indenting
215         //
216         // MultiTextEdit root;
217         // if (edit instanceof MultiTextEdit)
218         // root= (MultiTextEdit) edit;
219         // else {
220         // root= new MultiTextEdit(0, doc.getLength());
221         // root.addChild(edit);
222         // }
223         // for (Iterator it= offsets.iterator(); it.hasNext();) {
224         // TextEdit position= (TextEdit) it.next();
225         // try {
226         // root.addChild(position);
227         // } catch (MalformedTreeException e) {
228         // // position conflicts with formatter edit
229         // // ignore this position
230         // }
231         // }
232         //
233         // root.apply(doc, TextEdit.UPDATE_REGIONS);
234         //
235         // positionsToVariables(offsets, variables);
236         //
237         // templateBuffer.setContent(doc.get(), variables);
238         // }
239
240         private void indent(TemplateBuffer templateBuffer)
241                         throws BadLocationException, MalformedTreeException {
242
243                 TemplateVariable[] variables = templateBuffer.getVariables();
244                 List positions = variablesToPositions(variables);
245
246                 IDocument document = new Document(templateBuffer.getString());
247                 MultiTextEdit root = new MultiTextEdit(0, document.getLength());
248                 root.addChildren((TextEdit[]) positions.toArray(new TextEdit[positions
249                                 .size()]));
250
251                 JavaHeuristicScanner scanner = new JavaHeuristicScanner(document);
252                 JavaIndenter indenter = new JavaIndenter(document, scanner);
253
254                 // first line
255                 int offset = document.getLineOffset(0);
256                 TextEdit edit = new InsertEdit(offset, CodeFormatterUtil
257                                 .createIndentString(fInitialIndentLevel));
258                 root.addChild(edit);
259                 root.apply(document, TextEdit.UPDATE_REGIONS);
260                 root.removeChild(edit);
261
262                 formatDelimiter(document, root, 0);
263
264                 // following lines
265                 int lineCount = document.getNumberOfLines();
266
267                 for (int line = 1; line < lineCount; line++) {
268                         IRegion region = document.getLineInformation(line);
269                         offset = region.getOffset();
270                         StringBuffer indent = indenter.computeIndentation(offset);
271                         if (indent == null)
272                                 continue;
273                         // axelcl delete start
274                         // int nonWS =
275                         // scanner.findNonWhitespaceForwardInAnyPartition(offset, offset +
276                         // region.getLength());
277                         // if (nonWS == JavaHeuristicScanner.NOT_FOUND)
278                         // continue;
279                         // edit = new ReplaceEdit(offset, nonWS - offset,
280                         // indent.toString());
281                         // axelcl delete end
282                         // axelcl insert start
283                         int nonWS = offset;
284                         edit = new ReplaceEdit(offset, nonWS - offset, CodeFormatterUtil
285                                         .createIndentString(fInitialIndentLevel));
286                         // axelcl insert end
287                         root.addChild(edit);
288                         root.apply(document, TextEdit.UPDATE_REGIONS);
289                         root.removeChild(edit);
290
291                         formatDelimiter(document, root, line);
292                 }
293
294                 positionsToVariables(positions, variables);
295                 templateBuffer.setContent(document.get(), variables);
296         }
297
298         /**
299          * Changes the delimiter to the configured line delimiter.
300          * 
301          * @param document
302          *            the temporary document being edited
303          * @param root
304          *            the root edit containing all positions that will be updated
305          *            along the way
306          * @param line
307          *            the line to format
308          * @throws BadLocationException
309          *             if applying the changes fails
310          */
311         private void formatDelimiter(IDocument document, MultiTextEdit root,
312                         int line) throws BadLocationException {
313                 IRegion region = document.getLineInformation(line);
314                 String lineDelimiter = document.getLineDelimiter(line);
315                 if (lineDelimiter != null) {
316                         TextEdit edit = new ReplaceEdit(region.getOffset()
317                                         + region.getLength(), lineDelimiter.length(),
318                                         fLineDelimiter);
319                         root.addChild(edit);
320                         root.apply(document, TextEdit.UPDATE_REGIONS);
321                         root.removeChild(edit);
322                 }
323         }
324
325         private static void trimBegin(TemplateBuffer templateBuffer)
326                         throws BadLocationException {
327                 String string = templateBuffer.getString();
328                 TemplateVariable[] variables = templateBuffer.getVariables();
329
330                 List positions = variablesToPositions(variables);
331
332                 int i = 0;
333                 while ((i != string.length())
334                                 && Character.isWhitespace(string.charAt(i)))
335                         i++;
336
337                 string = edit(string, positions, new DeleteEdit(0, i));
338                 positionsToVariables(positions, variables);
339
340                 templateBuffer.setContent(string, variables);
341         }
342
343         private static String edit(String string, List positions, TextEdit edit)
344                         throws BadLocationException {
345                 MultiTextEdit root = new MultiTextEdit(0, string.length());
346                 root.addChildren((TextEdit[]) positions.toArray(new TextEdit[positions
347                                 .size()]));
348                 root.addChild(edit);
349                 IDocument document = new Document(string);
350                 root.apply(document);
351
352                 return document.get();
353         }
354
355         private static List variablesToPositions(TemplateVariable[] variables) {
356                 List positions = new ArrayList(5);
357                 for (int i = 0; i != variables.length; i++) {
358                         int[] offsets = variables[i].getOffsets();
359
360                         // trim positions off whitespace
361                         String value = variables[i].getDefaultValue();
362                         int wsStart = 0;
363                         while (wsStart < value.length()
364                                         && Character.isWhitespace(value.charAt(wsStart))
365                                         && !Strings.isLineDelimiterChar(value.charAt(wsStart)))
366                                 wsStart++;
367
368                         variables[i].getValues()[0] = value.substring(wsStart);
369
370                         for (int j = 0; j != offsets.length; j++) {
371                                 offsets[j] += wsStart;
372                                 positions.add(new RangeMarker(offsets[j], 0));
373                         }
374                 }
375                 return positions;
376         }
377
378         private static void positionsToVariables(List positions,
379                         TemplateVariable[] variables) {
380                 Iterator iterator = positions.iterator();
381
382                 for (int i = 0; i != variables.length; i++) {
383                         TemplateVariable variable = variables[i];
384
385                         int[] offsets = new int[variable.getOffsets().length];
386                         for (int j = 0; j != offsets.length; j++)
387                                 offsets[j] = ((TextEdit) iterator.next()).getOffset();
388
389                         variable.setOffsets(offsets);
390                 }
391         }
392 }