KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > eclipse > jdt > internal > ui > javaeditor > IndentUtil


1 /*******************************************************************************
2  * Copyright (c) 2005 IBM Corporation and others.
3  * All rights reserved. This program and the accompanying materials
4  * are made available under the terms of the Eclipse Public License v1.0
5  * which accompanies this distribution, and is available at
6  * http://www.eclipse.org/legal/epl-v10.html
7  *
8  * Contributors:
9  * IBM Corporation - initial API and implementation
10  *******************************************************************************/

11 package org.eclipse.jdt.internal.ui.javaeditor;
12
13 import org.eclipse.jface.text.BadLocationException;
14 import org.eclipse.jface.text.IDocument;
15 import org.eclipse.jface.text.IRegion;
16 import org.eclipse.jface.text.ITypedRegion;
17 import org.eclipse.jface.text.TextUtilities;
18 import org.eclipse.jface.text.source.ILineRange;
19
20 import org.eclipse.jdt.core.IJavaProject;
21
22 import org.eclipse.jdt.internal.corext.util.CodeFormatterUtil;
23
24 import org.eclipse.jdt.ui.text.IJavaPartitions;
25
26 import org.eclipse.jdt.internal.ui.text.JavaHeuristicScanner;
27 import org.eclipse.jdt.internal.ui.text.JavaIndenter;
28
29
30 /**
31  * Utility that indents a number of lines in a document.
32  *
33  * @since 3.1
34  */

35 public final class IndentUtil {
36     
37     private static final String JavaDoc SLASHES= "//"; //$NON-NLS-1$
38

39     /**
40      * The result of an indentation operation. The result may be passed to
41      * subsequent calls to
42      * {@link IndentUtil#indentLines(IDocument, ILineRange, IJavaProject, IndentResult) indentLines}
43      * to obtain consistent results with respect to the indentation of
44      * line-comments.
45      */

46     public static final class IndentResult {
47         private IndentResult(boolean[] commentLines) {
48             commentLinesAtColumnZero= commentLines;
49         }
50         private boolean[] commentLinesAtColumnZero;
51         private boolean hasChanged;
52         private int leftmostLine= -1;
53         /**
54          * Returns <code>true</code> if the indentation operation changed the
55          * document, <code>false</code> if not.
56          * @return <code>true</code> if the document was changed
57          */

58         public boolean hasChanged() {
59             return hasChanged;
60         }
61     }
62     
63     private IndentUtil() {
64         // do not instantiate
65
}
66
67     /**
68      * Indents the line range specified by <code>lines</code> in
69      * <code>document</code>. The passed Java project may be
70      * <code>null</code>, it is used solely to obtain formatter preferences.
71      *
72      * @param document the document to be changed
73      * @param lines the line range to be indented
74      * @param project the Java project to get the formatter preferences from, or
75      * <code>null</code> if global preferences should be used
76      * @param result the result from a previous call to <code>indentLines</code>,
77      * in order to maintain comment line properties, or <code>null</code>.
78      * Note that the passed result may be changed by the call.
79      * @return an indent result that may be queried for changes and can be
80      * reused in subsequent indentation operations
81      * @throws BadLocationException if <code>lines</code> is not a valid line
82      * range on <code>document</code>
83      */

84     public static IndentResult indentLines(IDocument document, ILineRange lines, IJavaProject project, IndentResult result) throws BadLocationException {
85         int numberOfLines= lines.getNumberOfLines();
86         
87         if (numberOfLines < 1)
88             return new IndentResult(null);
89         
90         result= reuseOrCreateToken(result, numberOfLines);
91         
92         JavaHeuristicScanner scanner= new JavaHeuristicScanner(document);
93         JavaIndenter indenter= new JavaIndenter(document, scanner, project);
94         boolean changed= false;
95         int tabSize= CodeFormatterUtil.getTabWidth(project);
96         for (int line= lines.getStartLine(), last= line + numberOfLines, i= 0; line < last; line++) {
97             changed |= indentLine(document, line, indenter, scanner, result.commentLinesAtColumnZero, i++, tabSize);
98         }
99         result.hasChanged= changed;
100         
101         return result;
102     }
103     
104     /**
105      * Shifts the line range specified by <code>lines</code> in
106      * <code>document</code>. The amount that the lines get shifted
107      * are determined by the first line in the range, all subsequent
108      * lines are adjusted accordingly. The passed Java project may be
109      * <code>null</code>, it is used solely to obtain formatter
110      * preferences.
111      *
112      * @param document the document to be changed
113      * @param lines the line range to be shifted
114      * @param project the Java project to get the formatter preferences
115      * from, or <code>null</code> if global preferences should
116      * be used
117      * @param result the result from a previous call to
118      * <code>shiftLines</code>, in order to maintain comment
119      * line properties, or <code>null</code>. Note that the
120      * passed result may be changed by the call.
121      * @return an indent result that may be queried for changes and can
122      * be reused in subsequent indentation operations
123      * @throws BadLocationException if <code>lines</code> is not a
124      * valid line range on <code>document</code>
125      */

126     public static IndentResult shiftLines(IDocument document, ILineRange lines, IJavaProject project, IndentResult result) throws BadLocationException {
127         int numberOfLines= lines.getNumberOfLines();
128         
129         if (numberOfLines < 1)
130             return new IndentResult(null);
131         
132         result= reuseOrCreateToken(result, numberOfLines);
133         result.hasChanged= false;
134         
135         JavaHeuristicScanner scanner= new JavaHeuristicScanner(document);
136         JavaIndenter indenter= new JavaIndenter(document, scanner, project);
137         
138         String JavaDoc current= getCurrentIndent(document, lines.getStartLine());
139         StringBuffer JavaDoc correct= indenter.computeIndentation(document.getLineOffset(lines.getStartLine()));
140         if (correct == null)
141             return result; // bail out
142

143         int tabSize= CodeFormatterUtil.getTabWidth(project);
144         StringBuffer JavaDoc addition= new StringBuffer JavaDoc();
145         int difference= subtractIndent(correct, current, addition, tabSize);
146         
147         if (difference == 0)
148             return result;
149             
150         if (result.leftmostLine == -1)
151             result.leftmostLine= getLeftMostLine(document, lines, tabSize);
152         
153         int maxReduction= computeVisualLength(getCurrentIndent(document, result.leftmostLine + lines.getStartLine()), tabSize);
154         
155         if (difference > 0) {
156             for (int line= lines.getStartLine(), last= line + numberOfLines, i= 0; line < last; line++)
157                 addIndent(document, line, addition, result.commentLinesAtColumnZero, i++);
158         } else {
159             int reduction= Math.min(-difference, maxReduction);
160             for (int line= lines.getStartLine(), last= line + numberOfLines, i= 0; line < last; line++)
161                 cutIndent(document, line, reduction, tabSize, result.commentLinesAtColumnZero, i++);
162         }
163                 
164         result.hasChanged= true;
165         
166         return result;
167         
168     }
169
170     /**
171      * Indents line <code>line</code> in <code>document</code> with <code>indent</code>.
172      * Leaves leading comment signs alone.
173      *
174      * @param document the document
175      * @param line the line
176      * @param indent the indentation to insert
177      * @param commentlines
178      * @throws BadLocationException on concurrent document modification
179      */

180     private static void addIndent(IDocument document, int line, CharSequence JavaDoc indent, boolean[] commentlines, int relative) throws BadLocationException {
181         IRegion region= document.getLineInformation(line);
182         int insert= region.getOffset();
183         int endOffset= region.getOffset() + region.getLength();
184
185         // go behind line comments
186
if (!commentlines[relative]) {
187             while (insert < endOffset - 2 && document.get(insert, 2).equals(SLASHES))
188                 insert += 2;
189         }
190
191         // insert indent
192
document.replace(insert, 0, indent.toString());
193     }
194
195     /**
196      * Cuts the visual equivalent of <code>toDelete</code> characters out of the
197      * indentation of line <code>line</code> in <code>document</code>. Leaves
198      * leading comment signs alone.
199      *
200      * @param document the document
201      * @param line the line
202      * @param toDelete the number of space equivalents to delete.
203      * @throws BadLocationException on concurrent document modification
204      */

205     private static void cutIndent(IDocument document, int line, int toDelete, int tabSize, boolean[] commentLines, int relative) throws BadLocationException {
206         IRegion region= document.getLineInformation(line);
207         int from= region.getOffset();
208         int endOffset= region.getOffset() + region.getLength();
209
210         // go behind line comments
211
while (from < endOffset - 2 && document.get(from, 2).equals(SLASHES))
212             from += 2;
213
214         int to= from;
215         while (toDelete > 0 && to < endOffset) {
216             char ch= document.getChar(to);
217             if (!Character.isWhitespace(ch))
218                 break;
219             toDelete -= computeVisualLength(ch, tabSize);
220             if (toDelete >= 0)
221                 to++;
222             else
223                 break;
224         }
225         
226         if (endOffset > to + 1 && document.get(to, 2).equals(SLASHES))
227             commentLines[relative]= true;
228
229         document.replace(from, to - from, null);
230     }
231
232     /**
233      * Computes the difference of two indentations and returns the difference in
234      * length of current and correct. If the return value is positive, <code>addition</code>
235      * is initialized with a substring of that length of <code>correct</code>.
236      *
237      * @param correct the correct indentation
238      * @param current the current indentation (migth contain non-whitespace)
239      * @param difference a string buffer - if the return value is positive, it will be cleared and set to the substring of <code>current</code> of that length
240      * @return the difference in lenght of <code>correct</code> and <code>current</code>
241      */

242     private static int subtractIndent(CharSequence JavaDoc correct, CharSequence JavaDoc current, StringBuffer JavaDoc difference, int tabSize) {
243         int c1= computeVisualLength(correct, tabSize);
244         int c2= computeVisualLength(current, tabSize);
245         int diff= c1 - c2;
246         if (diff <= 0)
247             return diff;
248
249         difference.setLength(0);
250         int len= 0, i= 0;
251         while (len < diff) {
252             char c= correct.charAt(i++);
253             difference.append(c);
254             len += computeVisualLength(c, tabSize);
255         }
256
257
258         return diff;
259     }
260
261     private static int computeVisualLength(char ch, int tabSize) {
262         if (ch == '\t')
263             return tabSize;
264         else
265             return 1;
266     }
267
268     /**
269      * Returns the visual length of a given <code>CharSequence</code> taking into
270      * account the visual tabulator length.
271      *
272      * @param seq the string to measure
273      * @return the visual length of <code>seq</code>
274      */

275     private static int computeVisualLength(CharSequence JavaDoc seq, int tablen) {
276         int size= 0;
277
278         for (int i= 0; i < seq.length(); i++) {
279             char ch= seq.charAt(i);
280             if (ch == '\t') {
281                 if (tablen != 0)
282                     size += tablen - size % tablen;
283                 // else: size stays the same
284
} else {
285                 size++;
286             }
287         }
288         return size;
289     }
290
291     /**
292      * Returns the indentation of the line <code>line</code> in <code>document</code>.
293      * The returned string may contain pairs of leading slashes that are considered
294      * part of the indentation. The space before the asterix in a javadoc-like
295      * comment is not considered part of the indentation.
296      *
297      * @param document the document
298      * @param line the line
299      * @return the indentation of <code>line</code> in <code>document</code>
300      * @throws BadLocationException if the document is changed concurrently
301      */

302     private static String JavaDoc getCurrentIndent(IDocument document, int line) throws BadLocationException {
303         IRegion region= document.getLineInformation(line);
304         int from= region.getOffset();
305         int endOffset= region.getOffset() + region.getLength();
306
307         // go behind line comments
308
int to= from;
309         while (to < endOffset - 2 && document.get(to, 2).equals(SLASHES))
310             to += 2;
311
312         while (to < endOffset) {
313             char ch= document.getChar(to);
314             if (!Character.isWhitespace(ch))
315                 break;
316             to++;
317         }
318
319         // don't count the space before javadoc like, asterix-style comment lines
320
if (to > from && to < endOffset - 1 && document.get(to - 1, 2).equals(" *")) { //$NON-NLS-1$
321
String JavaDoc type= TextUtilities.getContentType(document, IJavaPartitions.JAVA_PARTITIONING, to, true);
322             if (type.equals(IJavaPartitions.JAVA_DOC) || type.equals(IJavaPartitions.JAVA_MULTI_LINE_COMMENT))
323                 to--;
324         }
325
326         return document.get(from, to - from);
327     }
328     
329     private static int getLeftMostLine(IDocument document, ILineRange lines, int tabSize) throws BadLocationException {
330         int numberOfLines= lines.getNumberOfLines();
331         int first= lines.getStartLine();
332         int minLine= -1;
333         int minIndent= Integer.MAX_VALUE;
334         for (int line= 0; line < numberOfLines; line++) {
335             int length= computeVisualLength(getCurrentIndent(document, line + first), tabSize);
336             if (length < minIndent) {
337                 minIndent= length;
338                 minLine= line;
339             }
340         }
341         return minLine;
342     }
343
344     private static IndentResult reuseOrCreateToken(IndentResult token, int numberOfLines) {
345         if (token == null)
346             token= new IndentResult(new boolean[numberOfLines]);
347         else if (token.commentLinesAtColumnZero == null)
348             token.commentLinesAtColumnZero= new boolean[numberOfLines];
349         else if (token.commentLinesAtColumnZero.length != numberOfLines) {
350             boolean[] commentBooleans= new boolean[numberOfLines];
351             System.arraycopy(token.commentLinesAtColumnZero, 0, commentBooleans, 0, Math.min(numberOfLines, token.commentLinesAtColumnZero.length));
352             token.commentLinesAtColumnZero= commentBooleans;
353         }
354         return token;
355     }
356     
357     /**
358      * Indents a single line using the java heuristic scanner. Javadoc and multi
359      * line comments are indented as specified by the
360      * <code>JavaDocAutoIndentStrategy</code>.
361      *
362      * @param document the document
363      * @param line the line to be indented
364      * @param indenter the java indenter
365      * @param scanner the heuristic scanner
366      * @param commentLines the indent token comment booleans
367      * @param lineIndex the zero-based line index
368      * @return <code>true</code> if the document was modified,
369      * <code>false</code> if not
370      * @throws BadLocationException if the document got changed concurrently
371      */

372     private static boolean indentLine(IDocument document, int line, JavaIndenter indenter, JavaHeuristicScanner scanner, boolean[] commentLines, int lineIndex, int tabSize) throws BadLocationException {
373         IRegion currentLine= document.getLineInformation(line);
374         final int offset= currentLine.getOffset();
375         int wsStart= offset; // where we start searching for non-WS; after the "//" in single line comments
376

377         String JavaDoc indent= null;
378         if (offset < document.getLength()) {
379             ITypedRegion partition= TextUtilities.getPartition(document, IJavaPartitions.JAVA_PARTITIONING, offset, true);
380             ITypedRegion startingPartition= TextUtilities.getPartition(document, IJavaPartitions.JAVA_PARTITIONING, offset, false);
381             String JavaDoc type= partition.getType();
382             if (type.equals(IJavaPartitions.JAVA_DOC) || type.equals(IJavaPartitions.JAVA_MULTI_LINE_COMMENT)) {
383                 indent= computeJavadocIndent(document, line, scanner, startingPartition);
384             } else if (!commentLines[lineIndex] && startingPartition.getOffset() == offset && startingPartition.getType().equals(IJavaPartitions.JAVA_SINGLE_LINE_COMMENT)) {
385                 return false;
386             }
387         }
388         
389         // standard java indentation
390
if (indent == null) {
391             StringBuffer JavaDoc computed= indenter.computeIndentation(offset);
392             if (computed != null)
393                 indent= computed.toString();
394             else
395                 indent= new String JavaDoc();
396         }
397         
398         // change document:
399
// get current white space
400
int lineLength= currentLine.getLength();
401         int end= scanner.findNonWhitespaceForwardInAnyPartition(wsStart, offset + lineLength);
402         if (end == JavaHeuristicScanner.NOT_FOUND)
403             end= offset + lineLength;
404         int length= end - offset;
405         String JavaDoc currentIndent= document.get(offset, length);
406         
407         // memorize the fact that a line is a single line comment (but not at column 0) and should be treated like code
408
// as opposed to commented out code, which should keep its slashes at column 0
409
if (length > 0) {
410             ITypedRegion partition= TextUtilities.getPartition(document, IJavaPartitions.JAVA_PARTITIONING, end, false);
411             if (partition.getOffset() == end && IJavaPartitions.JAVA_SINGLE_LINE_COMMENT.equals(partition.getType())) {
412                 commentLines[lineIndex]= true;
413             }
414         }
415         
416         // only change the document if it is a real change
417
if (!indent.equals(currentIndent)) {
418             document.replace(offset, length, indent);
419             return true;
420         }
421         
422         return false;
423     }
424     
425     /**
426      * Computes and returns the indentation for a javadoc line. The line
427      * must be inside a javadoc comment.
428      *
429      * @param document the document
430      * @param line the line in document
431      * @param scanner the scanner
432      * @param partition the comment partition
433      * @return the indent, or <code>null</code> if not computable
434      * @throws BadLocationException
435      */

436     private static String JavaDoc computeJavadocIndent(IDocument document, int line, JavaHeuristicScanner scanner, ITypedRegion partition) throws BadLocationException {
437         if (line == 0) // impossible - the first line is never inside a javadoc comment
438
return null;
439         
440         // don't make any assumptions if the line does not start with \s*\* - it might be
441
// commented out code, for which we don't want to change the indent
442
final IRegion lineInfo= document.getLineInformation(line);
443         final int lineStart= lineInfo.getOffset();
444         final int lineLength= lineInfo.getLength();
445         final int lineEnd= lineStart + lineLength;
446         int nonWS= scanner.findNonWhitespaceForwardInAnyPartition(lineStart, lineEnd);
447         if (nonWS == JavaHeuristicScanner.NOT_FOUND || document.getChar(nonWS) != '*') {
448             if (nonWS == JavaHeuristicScanner.NOT_FOUND)
449                 return document.get(lineStart, lineLength);
450             return document.get(lineStart, nonWS - lineStart);
451         }
452         
453         // take the indent from the previous line and reuse
454
IRegion previousLine= document.getLineInformation(line - 1);
455         int previousLineStart= previousLine.getOffset();
456         int previousLineLength= previousLine.getLength();
457         int previousLineEnd= previousLineStart + previousLineLength;
458         
459         StringBuffer JavaDoc buf= new StringBuffer JavaDoc();
460         int previousLineNonWS= scanner.findNonWhitespaceForwardInAnyPartition(previousLineStart, previousLineEnd);
461         if (previousLineNonWS == JavaHeuristicScanner.NOT_FOUND || document.getChar(previousLineNonWS) != '*') {
462             // align with the comment start if the previous line is not an asterix line
463
previousLine= document.getLineInformationOfOffset(partition.getOffset());
464             previousLineStart= previousLine.getOffset();
465             previousLineLength= previousLine.getLength();
466             previousLineEnd= previousLineStart + previousLineLength;
467             previousLineNonWS= scanner.findNonWhitespaceForwardInAnyPartition(previousLineStart, previousLineEnd);
468             if (previousLineNonWS == JavaHeuristicScanner.NOT_FOUND)
469                 previousLineNonWS= previousLineEnd;
470             
471             // add the initial space
472
// TODO this may be controlled by a formatter preference in the future
473
buf.append(' ');
474         }
475         
476         String JavaDoc indentation= document.get(previousLineStart, previousLineNonWS - previousLineStart);
477         buf.insert(0, indentation);
478         return buf.toString();
479     }
480 }
481
Popular Tags