/*
* Copyright (c) 2012 Sam Harwell
* 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.tvl.netbeans.editor.whitespace;
import java.awt.Color;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.editor.mimelookup.MimeLookup;
import org.netbeans.api.editor.mimelookup.MimePath;
import org.netbeans.api.editor.mimelookup.MimeRegistration;
import org.netbeans.api.editor.settings.FontColorSettings;
import org.netbeans.spi.editor.highlighting.HighlightsLayer;
import org.netbeans.spi.editor.highlighting.HighlightsLayerFactory;
import org.netbeans.spi.editor.highlighting.HighlightsSequence;
import org.netbeans.spi.editor.highlighting.ZOrder;
import org.netbeans.spi.editor.highlighting.support.AbstractHighlightsContainer;
import org.openide.util.Lookup;
import org.openide.util.Parameters;
import org.openide.util.WeakListeners;
/**
*
* @author Sam Harwell
*/
public class WhitespaceHighlighter extends AbstractHighlightsContainer {
// -J-Dorg.tvl.netbeans.editor.whitespace.WhitespaceHighlighter.level=FINE
private static final Logger LOGGER = Logger.getLogger(WhitespaceHighlighter.class.getName());
private final StyledDocument document;
private final AttributeSet attributes;
protected WhitespaceHighlighter(@NonNull StyledDocument document) {
Parameters.notNull("document", document);
this.document = document;
Lookup lookup = MimeLookup.getLookup(MimePath.EMPTY);
FontColorSettings settings = lookup.lookup(FontColorSettings.class);
this.attributes = getFontAndColors(settings, "whitespace");
if (this.attributes != null) {
this.document.addDocumentListener(WeakListeners.document(new DocumentListenerImpl(), document));
}
}
protected StyledDocument getDocument() {
return document;
}
protected static AttributeSet getFontAndColors(FontColorSettings settings, String category) {
AttributeSet attributes = settings.getTokenFontColors(category);
return attributes;
}
@Override
public HighlightsSequence getHighlights(int startOffset, int endOffset) {
if (attributes == null) {
return HighlightsSequence.EMPTY;
}
return new HighlightsSequenceImpl(document, startOffset, endOffset, attributes);
}
@MimeRegistration(mimeType="", service=HighlightsLayerFactory.class)
public static class LayerFactory implements HighlightsLayerFactory {
@Override
public HighlightsLayer[] createLayers(Context context) {
Document document = context.getDocument();
if (!(document instanceof StyledDocument)) {
return new HighlightsLayer[0];
}
WhitespaceHighlighter highlighter = (WhitespaceHighlighter)document.getProperty(WhitespaceHighlighter.class);
if (highlighter == null) {
highlighter = createHighlighter(context);
document.putProperty(WhitespaceHighlighter.class, highlighter);
}
return new HighlightsLayer[] { HighlightsLayer.create(WhitespaceHighlighter.class.getName(), getPosition(), true, highlighter) };
}
protected WhitespaceHighlighter createHighlighter(Context context) {
return new WhitespaceHighlighter((StyledDocument)context.getDocument());
}
protected ZOrder getPosition() {
return ZOrder.SYNTAX_RACK.forPosition(1000);
}
}
public class DocumentListenerImpl implements DocumentListener {
@Override
public void insertUpdate(DocumentEvent e) {
fireHighlightsChange(e.getOffset(), e.getOffset() + e.getLength());
}
@Override
public void removeUpdate(DocumentEvent e) {
}
@Override
public void changedUpdate(DocumentEvent e) {
fireHighlightsChange(e.getOffset(), e.getOffset() + e.getLength());
}
}
protected static class HighlightsSequenceImpl implements HighlightsSequence {
private static final int BLOCK_SIZE = 1024;
private static final AttributeSet NEWLINE_ATTRIBUTES;
private final StyledDocument document;
private final int startOffset;
private final int endOffset;
private final AttributeSet attributes;
private final boolean includesEof;
private int currentOffset;
private int currentBlockOffset;
private String currentBlock;
private int currentWhitespaceStart;
private int currentWhitespaceEnd;
private boolean currentNewline;
private boolean finished;
static {
MutableAttributeSet attributes = new SimpleAttributeSet();
attributes.addAttribute(StyleConstants.Foreground, new Color(0, 0, 0, 0));
NEWLINE_ATTRIBUTES = attributes.copyAttributes();
}
private HighlightsSequenceImpl(@NonNull StyledDocument document, int startOffset, int endOffset, @NonNull AttributeSet attributes) {
Parameters.notNull("document", document);
Parameters.notNull("attributes", attributes);
this.document = document;
this.startOffset = startOffset;
int documentLength = document.getLength();
this.endOffset = Math.min(endOffset, documentLength);
this.includesEof = endOffset >= documentLength;
this.attributes = attributes;
this.currentOffset = startOffset;
this.currentBlockOffset = startOffset;
this.currentBlock = "";
}
@Override
public boolean moveNext() {
if (finished) {
return false;
}
int whitespaceStart = Integer.MAX_VALUE;
boolean inWhitespace = false;
boolean newline = false;
int effectiveEndOffset = endOffset + (includesEof ? 1 : 0);
searchLoop:
while (currentOffset < effectiveEndOffset) {
int offsetInBlock = currentOffset - currentBlockOffset;
while (offsetInBlock >= currentBlock.length()) {
int previousBlockEnd = currentBlockOffset + currentBlock.length();
if (!nextBlock()) {
break searchLoop;
}
int currentBlockEnd = currentBlockOffset + currentBlock.length();
assert currentBlockEnd > previousBlockEnd : "Whitespace search terminated due to lack of forward progress."; // NOI18N
if (currentBlockEnd <= previousBlockEnd) {
break searchLoop;
}
offsetInBlock = currentOffset - currentBlockOffset;
}
char c = currentBlock.charAt(offsetInBlock);
if (inWhitespace) {
if (newline && c != '\n' && c != '\r') {
break;
} else if (!newline && (!Character.isWhitespace(c) || c == '\n' || c == '\r')) {
break;
}
} else if (Character.isWhitespace(c)) {
whitespaceStart = currentOffset;
inWhitespace = true;
newline = c == '\r' || c == '\n';
}
currentOffset++;
}
if (currentOffset > whitespaceStart) {
currentWhitespaceStart = whitespaceStart;
currentWhitespaceEnd = currentOffset;
currentNewline = newline;
return true;
} else {
currentWhitespaceStart = currentOffset;
currentWhitespaceEnd = currentOffset;
currentNewline = false;
return false;
}
}
@Override
public int getStartOffset() {
return currentWhitespaceStart;
}
@Override
public int getEndOffset() {
return currentWhitespaceEnd;
}
@Override
public AttributeSet getAttributes() {
return currentNewline ? NEWLINE_ATTRIBUTES : attributes;
}
private boolean nextBlock() {
int effectiveEndOffset = endOffset + (includesEof ? 1 : 0);
int blockStart = currentBlockOffset + currentBlock.length();
int blockEnd = Math.min(effectiveEndOffset, blockStart + BLOCK_SIZE);
if (blockEnd == blockStart) {
currentBlock = "";
currentBlockOffset = endOffset;
finished = true;
return false;
}
try {
int boundedEnd = Math.min(blockEnd, endOffset);
if (boundedEnd == blockStart) {
currentBlock = "";
} else {
currentBlock = document.getText(blockStart, boundedEnd - blockStart);
}
if (boundedEnd < blockEnd) {
assert blockEnd == boundedEnd + 1;
assert blockEnd == effectiveEndOffset;
currentBlock += "\n";
}
currentBlockOffset = blockStart;
return true;
} catch (BadLocationException ex) {
LOGGER.log(Level.WARNING, "An exception occurred.", ex);
finished = true;
return false;
}
}
}
}