/*
* Copyright (c) 2012 Sam Harwell, Tunnel Vision Laboratories LLC
* All rights reserved.
*
* The source code of this document is proprietary work, and is not licensed for
* distribution. For information about licensing, contact Sam Harwell at:
* sam@tunnelvisionlabs.com
*/
package org.antlr.netbeans.editor.commenting;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.swing.text.BadLocationException;
import javax.swing.text.Position;
import javax.swing.text.Position.Bias;
import javax.swing.text.StyledDocument;
import org.antlr.netbeans.editor.DocumentLine;
import org.antlr.netbeans.editor.DocumentPoint;
import org.antlr.netbeans.editor.DocumentSpan;
import org.antlr.netbeans.editor.TextEdit;
import org.netbeans.editor.BaseDocument;
import org.openide.util.Parameters;
/**
*
* @author Sam Harwell
*/
public class StandardCommenter implements Commenter {
private final List<CommentFormat> commentFormats;
private final List<BlockCommentFormat> blockFormats;
private final List<LineCommentFormat> lineFormats;
private final boolean useLineComments;
public StandardCommenter(CommentFormat... commentFormats) {
this.commentFormats = Arrays.asList(Arrays.copyOf(commentFormats, commentFormats.length));
this.blockFormats = new ArrayList<>();
this.lineFormats = new ArrayList<>();
for (CommentFormat format : commentFormats) {
if (format instanceof BlockCommentFormat) {
blockFormats.add((BlockCommentFormat)format);
} else if (format instanceof LineCommentFormat) {
lineFormats.add((LineCommentFormat)format);
}
}
this.useLineComments = !this.lineFormats.isEmpty();
}
public List<CommentFormat> getCommentFormats() {
return commentFormats;
}
public List<BlockCommentFormat> getBlockFormats() {
return blockFormats;
}
public List<LineCommentFormat> getLineFormats() {
return lineFormats;
}
public BlockCommentFormat getPreferredBlockFormat() {
List<BlockCommentFormat> formats = getBlockFormats();
if (formats == null || formats.isEmpty()) {
return null;
}
return formats.get(0);
}
public LineCommentFormat getPreferredLineFormat() {
List<LineCommentFormat> formats = getLineFormats();
if (formats == null || formats.isEmpty()) {
return null;
}
return formats.get(0);
}
public boolean getUseLineComments() {
return useLineComments;
}
@Override
public List<DocumentSpan> commentSpans(List<DocumentSpan> spans) throws BadLocationException {
Parameters.notNull("spans", spans);
List<DocumentSpan> result = new ArrayList<>();
if (spans.isEmpty())
return result;
List<Position> startPositions = new ArrayList<>();
List<Position> endPositions = new ArrayList<>();
StyledDocument document = spans.get(0).getDocument();
if (!(document instanceof BaseDocument)) {
return spans;
}
BaseDocument baseDocument = (BaseDocument)document;
TextEdit edit = createEdit(baseDocument);
try {
for (DocumentSpan span : spans) {
DocumentSpan selection = commentSpan(span, edit);
if (baseDocument != null) {
startPositions.add(baseDocument.createPosition(selection.getStart().getOffset(), Bias.Backward));
} else {
startPositions.add(document.createPosition(selection.getStart().getOffset()));
}
endPositions.add(document.createPosition(selection.getEnd().getOffset()));
}
edit.apply();
} finally {
edit.dispose();
}
for (int i = 0; i < startPositions.size(); i++) {
result.add(new DocumentSpan(document, startPositions.get(i).getOffset(), endPositions.get(i).getOffset()));
}
if (result.size() > 1) {
for (int i = result.size() - 1; result.size() > 1 && i >= 0; i--) {
if (result.get(i).isEmpty()) {
result.remove(i);
}
}
}
return result;
}
@Override
public List<DocumentSpan> uncommentSpans(List<DocumentSpan> spans) throws BadLocationException {
Parameters.notNull("spans", spans);
List<DocumentSpan> result = new ArrayList<>();
if (spans.isEmpty())
return result;
List<Position> startPositions = new ArrayList<>();
List<Position> endPositions = new ArrayList<>();
StyledDocument document = spans.get(0).getDocument();
if (!(document instanceof BaseDocument)) {
return spans;
}
BaseDocument baseDocument = (BaseDocument)document;
TextEdit edit = createEdit(baseDocument);
try {
for (DocumentSpan span : spans) {
DocumentSpan selection = uncommentSpan(span, edit);
if (baseDocument != null) {
startPositions.add(baseDocument.createPosition(selection.getStart().getOffset(), Bias.Backward));
} else {
startPositions.add(document.createPosition(selection.getStart().getOffset()));
}
endPositions.add(document.createPosition(selection.getEnd().getOffset()));
}
edit.apply();
} finally {
edit.dispose();
}
for (int i = 0; i < startPositions.size(); i++) {
result.add(new DocumentSpan(document, startPositions.get(i).getOffset(), endPositions.get(i).getOffset()));
}
if (result.size() > 1) {
for (int i = result.size() - 1; result.size() > 1 && i >= 0; i--) {
if (result.get(i).isEmpty()) {
result.remove(i);
}
}
}
return result;
}
protected DocumentSpan commentSpan(DocumentSpan span, TextEdit edit) throws BadLocationException {
Parameters.notNull("span", span);
Parameters.notNull("edit", edit);
/*
* Use line comments if:
* UseLineComments is true
* AND one of the following is true:
*
* 1. there is no selected text
* 2. on the line where the selection starts, there is only whitespace up to the selection start point
* AND on the line where the selection ends, there is only whitespace up to the selection end point,
* OR there is only whitespace from the selection end point to the end of the line
*
* Use block comments if:
* We are not using line comments
* AND some text is selected
* AND PreferredBlockFormat is not null
*/
DocumentLine startContainingLine = span.getStart().getContainingLine();
DocumentLine endContainingLine = span.getEnd().getContainingLine();
if (getUseLineComments()
&& (span.isEmpty() ||
(isNullOrWhiteSpace(startContainingLine.getText().substring(0, span.getStart().subtract(startContainingLine.getStart())))
&& (isNullOrWhiteSpace(endContainingLine.getText().substring(0, span.getEnd().subtract(endContainingLine.getStart())))
|| isNullOrWhiteSpace(endContainingLine.getText().substring(span.getEnd().subtract(endContainingLine.getStart()))))
)))
{
span = commentLines(span, edit, getPreferredLineFormat());
}
else if (
span.getLength() > 0
&& getPreferredBlockFormat() != null
)
{
span = commentBlock(span, edit, getPreferredBlockFormat());
}
return span;
}
protected DocumentSpan commentLines(DocumentSpan span, TextEdit edit, LineCommentFormat format) throws BadLocationException {
Parameters.notNull("span", span);
Parameters.notNull("edit", edit);
Parameters.notNull("format", format);
/* Rules for line comments:
* Make sure line comments are indented as far as possible, skipping empty lines as necessary
* Don't comment N+1 lines when only N lines were selected by clicking in the left margin
*/
if (span.getEnd().getContainingLine().getLineNumber() > span.getStart().getContainingLine().getLineNumber()
&& span.getEnd().getContainingLine().getStart().equals(span.getEnd())) {
DocumentPoint start = span.getStart();
DocumentPoint end = getLineFromLineNumber(span.getDocument(), span.getEnd().getContainingLine().getLineNumber() - 1).getStart();
if (end.compareTo(start) < 0) {
start = end;
}
span = new DocumentSpan(start, end);
}
int minIndex = Integer.MAX_VALUE;
int firstLineNumber = span.getStart().getContainingLine().getLineNumber();
int lineCount = span.getEnd().getContainingLine().getLineNumber() - span.getStart().getContainingLine().getLineNumber() + 1;
for (int i = firstLineNumber; i < firstLineNumber + lineCount; i++) {
DocumentLine line = getLineFromLineNumber(span.getDocument(), i);
if (line.getText().trim().isEmpty()) {
continue;
}
int index = scanToNonWhitespaceChar(line);
minIndex = Math.min(minIndex, index);
}
// comment each line
for (int line = span.getStart().getContainingLine().getLineNumber(); line <= span.getEnd().getContainingLine().getLineNumber(); line++) {
DocumentLine l = getLineFromLineNumber(span.getDocument(), line);
if (l.getText().trim().isEmpty()) {
continue;
}
edit.insert(l.getStart().add(minIndex), format.getStartText());
}
return new DocumentSpan(span.getStart().getContainingLine().getStart(), span.getEnd().getContainingLine().getEnd());
}
protected DocumentSpan commentBlock(DocumentSpan span, TextEdit edit, BlockCommentFormat format) throws BadLocationException {
Parameters.notNull("span", span);
Parameters.notNull("edit", edit);
Parameters.notNull("format", format);
// special case no selection
if (span.isEmpty()) {
span = new DocumentSpan(span.getStart().getContainingLine().getStart().add(scanToNonWhitespaceChar(span.getStart().getContainingLine())), span.getEnd().getContainingLine().getEnd());
}
// add start comment
edit.insert(span.getStart(), format.getStartText());
// add end comment
edit.insert(span.getEnd(), format.getEndText());
return span;
}
protected DocumentSpan uncommentSpan(DocumentSpan span, TextEdit edit) throws BadLocationException {
Parameters.notNull("span", span);
@SuppressWarnings("LocalVariableHidesMemberVariable")
boolean useLineComments = true;
DocumentLine startContainingLine = span.getStart().getContainingLine();
DocumentLine endContainingLine = span.getEnd().getContainingLine();
// special case: empty span
if (span.isEmpty()) {
if (useLineComments) {
span = uncommentLines(span, edit, getLineFormats());
}
} else {
DocumentSpan resultSpan = tryUncommentBlock(span, edit, getBlockFormats());
if (resultSpan != null) {
return resultSpan;
}
if (useLineComments) {
span = uncommentLines(span, edit, getLineFormats());
}
}
return span;
}
protected DocumentSpan uncommentLines(DocumentSpan span, TextEdit edit, List<LineCommentFormat> formats) throws BadLocationException {
Parameters.notNull("span", span);
Parameters.notNull("formats", formats);
if (span.getEnd().getContainingLine().getLineNumber() > span.getStart().getContainingLine().getLineNumber()
&& span.getEnd().equals(span.getEnd().getContainingLine().getStart())) {
DocumentPoint start = span.getStart();
DocumentPoint end = span.getEnd().getContainingLine().getStart().subtract(1).getContainingLine().getStart();
if (end.compareTo(start) < 0) {
start = end;
}
span = new DocumentSpan(start, end);
}
// remove line comments
for (int line = span.getStart().getContainingLine().getLineNumber(); line <= span.getEnd().getContainingLine().getLineNumber(); line++) {
DocumentLine currentLine = getLineFromLineNumber(span.getDocument(), line);
int i = scanToNonWhitespaceChar(currentLine);
String text = currentLine.getText();
for (LineCommentFormat format : formats) {
int commentLength = format.getStartText().length();
if ((text.length() > i + commentLength) && text.substring(i, i + commentLength).equals(format.getStartText())) {
// remove line comment
edit.remove(currentLine.getStart().getOffset() + i, commentLength);
}
}
}
return new DocumentSpan(span.getStart().getContainingLine().getStart(), span.getEnd().getContainingLine().getEnd());
}
protected DocumentSpan tryUncommentBlock(DocumentSpan span, TextEdit edit, List<BlockCommentFormat> formats) throws BadLocationException {
Parameters.notNull("span", span);
Parameters.notNull("formats", formats);
for (BlockCommentFormat format : formats) {
DocumentSpan result = tryUncommentBlock(span, edit, format);
if (result != null) {
return result;
}
}
return null;
}
protected DocumentSpan tryUncommentBlock(DocumentSpan span, TextEdit edit, BlockCommentFormat format) throws BadLocationException {
Parameters.notNull("span", span);
Parameters.notNull("format", format);
String blockStart = format.getStartText();
String blockEnd = format.getEndText();
int startLength = span.getStart().getContainingLine().getLength();
int endLength = span.getEnd().getContainingLine().getLength();
span = trimSpan(span);
// special case no selection: try and uncomment the current line
if (span.isEmpty()) {
span = new DocumentSpan(span.getStart().getContainingLine().getStart().add(scanToNonWhitespaceChar(span.getStart().getContainingLine())), span.getEnd().getContainingLine().getEnd());
}
// check that the comment start and end blocks are possible
if ((span.getStart().subtract(span.getStart().getContainingLine().getStart())) + blockStart.length() <= startLength
&& (span.getEnd().subtract(span.getEnd().getContainingLine().getStart())) - blockStart.length() >= 0) {
String startText = span.getDocument().getText(span.getStart().getOffset(), blockStart.length());
if (startText.equals(blockStart)) {
DocumentSpan linespan = new DocumentSpan(span.getEnd().subtract(blockEnd.length()), span.getEnd());
String endText = linespan.getText();
if (endText.equals(blockEnd)) {
// yes, block comment selected, remove it
edit.remove(linespan.getStart().getOffset(), linespan.getLength());
edit.remove(span.getStart().getOffset(), blockStart.length());
return span;
}
}
}
return null;
}
private TextEdit createEdit(BaseDocument document) {
Parameters.notNull("document", document);
return new TextEditImpl(document);
}
private static boolean isNullOrWhiteSpace(String text) {
if (text == null) {
return true;
}
return text.isEmpty() || text.trim().isEmpty();
}
protected static DocumentLine getLineFromLineNumber(StyledDocument document, int lineNumber) throws BadLocationException {
return new DocumentLine(document, lineNumber);
}
protected static DocumentSpan trimSpan(DocumentSpan span) throws BadLocationException {
String text = span.getText();
int length = text.trim().length();
int offset = 0;
while (offset < text.length() && Character.isWhitespace(text.charAt(offset))) {
offset++;
}
if (offset > 0 || length != text.length()) {
DocumentPoint start = span.getStart().add(offset);
return new DocumentSpan(start, start.add(length));
}
return span;
}
protected static int scanToNonWhitespaceChar(DocumentLine line) throws BadLocationException {
Parameters.notNull("line", line);
String text = line.getText();
int length = text.length();
int i = 0;
while (i < length && Character.isWhitespace(text.charAt(i))) {
i++;
}
return i;
}
}