blob: 90eb818b331d1d1079986144366e0a39881b3bb3 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2004 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.internal.formatter.comment;
import java.util.Iterator;
import java.util.LinkedList;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DefaultLineTracker;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ILineTracker;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jdt.internal.formatter.CodeFormatterVisitor;
import org.eclipse.jdt.internal.formatter.DefaultCodeFormatterOptions;
import org.eclipse.jdt.internal.formatter.Scribe;
/**
* Comment region in a source code document.
*
* @since 3.0
*/
public class CommentRegion extends Position implements IHtmlTagDelimiters, IBorderAttributes, ICommentAttributes {
/** Default comment range delimiter */
protected static final String COMMENT_RANGE_DELIMITER= " "; //$NON-NLS-1$
/** Default line prefix length */
private static final int COMMENT_PREFIX_LENGTH= 3;
/** The borders of this region */
private int fBorders= 0;
/** Should all blank lines be cleared during formatting? */
private final boolean fClear;
/** The line delimiter used in this comment region */
private final String fDelimiter;
/** The document to format */
private final IDocument fDocument;
/** The lines in this comment region */
private final LinkedList fLines= new LinkedList();
/** The formatting preferences */
protected final DefaultCodeFormatterOptions preferences;
/** The comment ranges in this comment region */
private final LinkedList fRanges= new LinkedList();
/** Is this comment region a single line region? */
private final boolean fSingleLine;
/** Number of spaces representing tabulator */
private int fTabSize;
/**
* <code>true</code> if tabs, not spaces, should be used for indentation
* @since 3.1
*/
private boolean fUseTab;
/** the scribe used to create edits */
protected Scribe scribe;
/**
* Creates a new comment region.
*
* @param document the document which contains the comment region
* @param position the position of this comment region in the document
* @param formatter the given code formatter
*/
public CommentRegion(final IDocument document, final Position position, final CodeFormatterVisitor formatter) {
super(position.getOffset(), position.getLength());
this.preferences = formatter.preferences;
fDelimiter = this.preferences.line_separator;
fDocument= document;
fClear= this.preferences.comment_clear_blank_lines;
fTabSize= this.preferences.tab_size;
fUseTab = this.preferences.tab_char == DefaultCodeFormatterOptions.TAB;
this.scribe = formatter.scribe;
final ILineTracker tracker= new DefaultLineTracker();
IRegion range= null;
CommentLine line= null;
tracker.set(getText(0, getLength()));
final int lines= tracker.getNumberOfLines();
fSingleLine= lines == 1;
try {
for (int index= 0; index < lines; index++) {
range= tracker.getLineInformation(index);
line= createLine();
line.append(new CommentRange(range.getOffset(), range.getLength()));
fLines.add(line);
}
} catch (BadLocationException exception) {
// Should not happen
}
}
/**
* Appends the comment range to this comment region.
*
* @param range comment range to append to this comment region
*/
protected final void append(final CommentRange range) {
fRanges.addLast(range);
}
/**
* Can the comment range be appended to the comment line?
*
* @param line comment line where to append the comment range
* @param previous comment range which is the predecessor of the current
* comment range
* @param next comment range to test whether it can be appended to the
* comment line
* @param index amount of space in the comment line used by already
* inserted comment ranges
* @param width the maximal width of text in this comment region
* measured in average character widths
* @return <code>true</code> iff the comment range can be added to the
* line, <code>false</code> otherwise
*/
protected boolean canAppend(final CommentLine line, final CommentRange previous, final CommentRange next, final int index, final int width) {
return index == 0 || index + next.getLength() <= width;
}
/**
* Can the whitespace between the two comment ranges be formatted?
*
* @param previous previous comment range which was already formatted,
* can be <code>null</code>
* @param next next comment range to be formatted
* @return <code>true</code> iff the next comment range can be
* formatted, <code>false</code> otherwise.
*/
protected boolean canFormat(final CommentRange previous, final CommentRange next) {
return previous != null;
}
/**
* Formats the comment region with the given indentation level.
*
* @param indentationLevel the indentation level
* @return the resulting text edit of the formatting process
* @since 3.1
*/
public final TextEdit format(int indentationLevel, boolean returnEdit) {
final String probe= getText(0, CommentLine.NON_FORMAT_START_PREFIX.length());
if (!probe.startsWith(CommentLine.NON_FORMAT_START_PREFIX)) {
int margin= this.preferences.comment_line_length;
String indentation= computeIndentation(indentationLevel);
margin= Math.max(COMMENT_PREFIX_LENGTH + 1, margin - stringToLength(indentation) - COMMENT_PREFIX_LENGTH);
tokenizeRegion();
markRegion();
wrapRegion(margin);
formatRegion(indentation, margin);
}
if (returnEdit) {
return this.scribe.getRootEdit();
}
return null;
}
/**
* Formats this comment region.
*
* @param indentation the indentation of this comment region
* @param width the maximal width of text in this comment region
* measured in average character widths
*/
protected void formatRegion(final String indentation, final int width) {
final int last= fLines.size() - 1;
if (last >= 0) {
CommentLine lastLine= (CommentLine)fLines.get(last);
CommentRange lastRange= lastLine.getLast();
lastLine.formatLowerBorder(lastRange, indentation, width);
CommentLine previous;
CommentLine next= null;
CommentRange range= null;
for (int line= last; line >= 0; line--) {
previous= next;
next= (CommentLine)fLines.get(line);
range= next.formatLine(previous, range, indentation, line);
}
next.formatUpperBorder(range, indentation, width);
}
}
/**
* Returns the line delimiter used in this comment region.
*
* @return the line delimiter for this comment region
*/
protected final String getDelimiter() {
return fDelimiter;
}
/**
* Returns the line delimiter used in this comment line break.
*
* @param predecessor the predecessor comment line after the line break
* @param successor the successor comment line before the line break
* @param previous the comment range after the line break
* @param next the comment range before the line break
* @param indentation indentation of the formatted line break
* @return the line delimiter for this comment line break
*/
protected String getDelimiter(final CommentLine predecessor, final CommentLine successor, final CommentRange previous, final CommentRange next, final String indentation) {
return fDelimiter + indentation + successor.getContentPrefix();
}
/**
* Returns the range delimiter for this comment range break.
*
* @param previous the previous comment range to the right of the range
* delimiter
* @param next the next comment range to the left of the range delimiter
* @return the delimiter for this comment range break
*/
protected String getDelimiter(final CommentRange previous, final CommentRange next) {
return COMMENT_RANGE_DELIMITER;
}
/**
* Returns the document of this comment region.
*
* @return the document of this region
*/
protected final IDocument getDocument() {
return fDocument;
}
/**
* Returns the comment ranges in this comment region
*
* @return the comment ranges in this region
*/
protected final LinkedList getRanges() {
return fRanges;
}
/**
* Returns the number of comment lines in this comment region.
*
* @return the number of lines in this comment region
*/
protected final int getSize() {
return fLines.size();
}
/**
* Returns the text of this comment region in the indicated range.
*
* @param position the offset of the comment range to retrieve in
* comment region coordinates
* @param count the length of the comment range to retrieve
* @return the content of this comment region in the indicated range
*/
protected final String getText(final int position, final int count) {
String content= ""; //$NON-NLS-1$
try {
content= fDocument.get(getOffset() + position, count);
} catch (BadLocationException exception) {
// Should not happen
}
return content;
}
/**
* Does the border <code>border</code> exist?
*
* @param border the type of the border, must be a border attribute of
* <code>CommentRegion</code>
* @return <code>true</code> iff this border exists,
* <code>false</code> otherwise
*/
protected final boolean hasBorder(final int border) {
return (fBorders & border) == border;
}
/**
* Does the comment range consist of letters and digits only?
*
* @param range the comment range to text
* @return <code>true</code> iff the comment range consists of letters
* and digits only, <code>false</code> otherwise
*/
protected final boolean isAlphaNumeric(final CommentRange range) {
final String token= getText(range.getOffset(), range.getLength());
for (int index= 0; index < token.length(); index++) {
if (!Character.isLetterOrDigit(token.charAt(index)))
return false;
}
return true;
}
/**
* Does the comment range contain no letters and digits?
*
* @param range the comment range to text
* @return <code>true</code> iff the comment range contains no letters
* and digits, <code>false</code> otherwise
*/
protected final boolean isNonAlphaNumeric(final CommentRange range) {
final String token= getText(range.getOffset(), range.getLength());
for (int index= 0; index < token.length(); index++) {
if (Character.isLetterOrDigit(token.charAt(index)))
return false;
}
return true;
}
/**
* Should blank lines be cleared during formatting?
*
* @return <code>true</code> iff blank lines should be cleared,
* <code>false</code> otherwise
*/
protected final boolean isClearLines() {
return fClear;
}
/**
* Is this comment region a single line region?
*
* @return <code>true</code> iff this region is single line,
* <code>false</code> otherwise
*/
protected final boolean isSingleLine() {
return fSingleLine;
}
/**
* Logs a text edit operation occurred during the formatting process
*
* @param change the changed text
* @param position offset measured in comment region coordinates where
* to apply the changed text
* @param count length of the range where to apply the changed text
*/
protected final void logEdit(final String change, final int position, final int count) {
try {
final int base= getOffset() + position;
final String content= fDocument.get(base, count);
if (!change.equals(content)) {
if (count > 0) {
this.scribe.addReplaceEdit(base, base + count - 1, change);
} else {
this.scribe.addInsertEdit(base, change);
}
}
} catch (BadLocationException exception) {
// Should not happen
CommentFormatterUtil.log(exception);
} catch (MalformedTreeException exception) {
// Do nothing
CommentFormatterUtil.log(exception);
}
}
/**
* Marks the comment ranges in this comment region.
*/
protected void markRegion() {
// Do nothing
}
/**
* Set the border type <code>border</code> to true.
*
* @param border the type of the border. Must be a border attribute of
* <code>CommentRegion</code>
*/
protected final void setBorder(final int border) {
fBorders |= border;
}
/**
* Returns the indentation of the given indentation level.
*
* @param indentationLevel the indentation level
* @return the indentation of the given indentation level
* @since 3.1
*/
private String computeIndentation(int indentationLevel) {
return replicate(fUseTab ? "\t" : replicate(" ", fTabSize), indentationLevel); //$NON-NLS-1$//$NON-NLS-2$
}
/**
* Returns the given string n-times replicated.
*
* @param string the string
* @param n n
* @return the given string n-times replicated
* @since 3.1
*/
private String replicate(String string, int n) {
StringBuffer buffer= new StringBuffer(n*string.length());
for (int i= 0; i < n; i++)
buffer.append(string);
return buffer.toString();
}
/**
* Computes the equivalent indentation for a string
*
* @param reference the string to compute the indentation for
* @param tabs <code>true</code> iff the indentation should use tabs,
* <code>false</code> otherwise.
* @return the indentation string
*/
protected final String stringToIndent(final String reference, final boolean tabs) {
int spaceWidth= 1;
int referenceWidth= expandTabs(reference).length();
final StringBuffer buffer= new StringBuffer();
final int spaces= referenceWidth / spaceWidth;
if (tabs) {
final int count= spaces / fTabSize;
final int modulo= spaces % fTabSize;
for (int index= 0; index < count; index++)
buffer.append('\t');
for (int index= 0; index < modulo; index++)
buffer.append(' ');
} else {
for (int index= 0; index < spaces; index++)
buffer.append(' ');
}
return buffer.toString();
}
/**
* Returns the length of the string in expanded characters.
*
* @param reference the string to get the length for
* @return the length of the string in expanded characters
*/
protected final int stringToLength(final String reference) {
return expandTabs(reference).length();
}
/**
* Expands the given string's tabs according to the given tab size.
*
* @param string the string
* @return the expanded string
* @since 3.1
*/
private String expandTabs(String string) {
StringBuffer expanded= new StringBuffer();
for (int i= 0, n= string.length(), chars= 0; i < n; i++) {
char ch= string.charAt(i);
if (ch == '\t') {
for (; chars < fTabSize; chars++)
expanded.append(' ');
chars= 0;
} else {
expanded.append(ch);
chars++;
if (chars >= fTabSize)
chars= 0;
}
}
return expanded.toString();
}
/**
* Tokenizes the comment region.
*/
protected void tokenizeRegion() {
int index= 0;
CommentLine line= null;
for (final Iterator iterator= fLines.iterator(); iterator.hasNext(); index++) {
line= (CommentLine)iterator.next();
line.scanLine(index);
line.tokenizeLine(index);
}
}
/**
* Wraps the comment ranges in this comment region into comment lines.
*
* @param width the maximal width of text in this comment region
* measured in average character widths
*/
protected void wrapRegion(final int width) {
fLines.clear();
int index= 0;
boolean adapted= false;
CommentLine successor= null;
CommentLine predecessor= null;
CommentRange previous= null;
CommentRange next= null;
while (!fRanges.isEmpty()) {
index= 0;
adapted= false;
predecessor= successor;
successor= createLine();
fLines.add(successor);
while (!fRanges.isEmpty()) {
next= (CommentRange)fRanges.getFirst();
if (canAppend(successor, previous, next, index, width)) {
if (!adapted && predecessor != null) {
successor.adapt(predecessor);
adapted= true;
}
fRanges.removeFirst();
successor.append(next);
index += (next.getLength() + 1);
previous= next;
} else
break;
}
}
}
/**
* Creates a new line for this region.
*
* @return a new line for this region
* @since 3.1
*/
protected CommentLine createLine() {
return new SingleCommentLine(this);
}
}