/*
* 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.text;
import javax.swing.text.Document;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.editor.BaseDocument;
/**
*
* @author Sam Harwell
*/
public final class DocumentTextUtilities {
private DocumentTextUtilities() {
}
/** Get the identifier around the given position or null if there's no identifier
* around the given position. The identifier must be
* accepted by SyntaxSupport.isIdnetifier() otherwise null is returned.
* @param position position in document - usually the caret.getDot()
* @return the block (starting and ending position) enclosing the identifier
* or null if no identifier was found
*/
@SuppressWarnings("deprecation")
public static @CheckForNull SnapshotPositionRegion getIdentifierBlock(@NonNull SnapshotPosition position) {
DocumentSnapshot snapshot = position.getSnapshot();
Document document = snapshot.getVersionedDocument().getDocument();
if (!(document instanceof BaseDocument)) {
return null;
}
BaseDocument baseDocument = (BaseDocument)document;
SnapshotPosition idStart = getWordStart(position);
if (idStart != null) {
SnapshotPosition idEnd = getWordEnd(idStart);
if (idEnd != null) {
SnapshotPositionRegion region = new SnapshotPositionRegion(idStart, idEnd);
String id = region.getText();
if (baseDocument.getSyntaxSupport().isIdentifier(id)) {
return region;
} else { // not identifier by syntax support
id = getWord(position); // try right at offset
if (baseDocument.getSyntaxSupport().isIdentifier(id)) {
assert id != null;
return new SnapshotPositionRegion(position, id.length());
}
}
}
}
return null;
}
public static @CheckForNull SnapshotPosition getWordStart(@NonNull SnapshotPosition position) {
Document document = position.getSnapshot().getVersionedDocument().getDocument();
if (!(document instanceof BaseDocument)) {
return null;
}
return find(new SnapshotFinderFactory.PreviousWordBwdFinder((BaseDocument)document, false, true),
position, SearchDirection.BACKWARD);
}
public static @CheckForNull SnapshotPosition getWordEnd(@NonNull SnapshotPosition position) {
Document document = position.getSnapshot().getVersionedDocument().getDocument();
if (!(document instanceof BaseDocument)) {
return null;
}
SnapshotPosition ret = find(new SnapshotFinderFactory.NextWordFwdFinder((BaseDocument)document, false, true),
position, SearchDirection.FORWARD);
return (ret != null) ? ret : new SnapshotPosition(position.getSnapshot(), position.getSnapshot().length());
}
/** Get the word at given position.
* @param position
* @return
*/
public static @CheckForNull String getWord(@NonNull SnapshotPosition position) {
SnapshotPosition wordEnd = getWordEnd(position);
if (wordEnd != null) {
DocumentSnapshot snapshot = position.getSnapshot();
return snapshot.subSequence(position.getOffset(), wordEnd.getOffset()).toString();
}
return null;
}
public static @CheckForNull SnapshotPosition find(@NonNull SnapshotFinder finder, @NonNull SnapshotPosition position) {
return find(finder, position, SearchDirection.FORWARD);
}
public static @CheckForNull SnapshotPosition find(@NonNull SnapshotFinder finder, @NonNull SnapshotPosition position, @NonNull SearchDirection direction) {
if (direction == SearchDirection.FORWARD) {
int snapshotLength = position.getSnapshot().length();
return find(finder, new SnapshotPositionRegion(position, snapshotLength - position.getOffset()), direction);
} else {
return find(finder, new SnapshotPositionRegion(position.getSnapshot(), 0, position.getOffset()), direction);
}
}
public static @CheckForNull SnapshotPosition find(@NonNull SnapshotFinder finder, @NonNull SnapshotPositionRegion region, @NonNull SearchDirection direction) {
DocumentSnapshot snapshot = region.getSnapshot();
if (finder instanceof SnapshotAdjustFinder) {
if (region.isEmpty()) { // stop immediately
finder.reset(); // reset() should be called in all the cases
return null; // must stop here because wouldn't know if fwd/bwd search?
}
region = ((SnapshotAdjustFinder)finder).adjustPositionRegion(region);
if (region == null) {
finder.reset();
return null;
}
}
finder.reset();
if (region.getLength() == 0) {
return null;
}
// CharSequence text = new javax.swing.text.Segment();
int gapStart = 0;
// int gapStart = ((EditorDocumentContent)getContent()).getCharContentGapStart();
// if (gapStart == -1) {
// throw new IllegalStateException("Cannot get gapStart"); // NOI18N
// }
int startPos = region.getStart().getOffset();
int limitPos = region.getEnd().getOffset();
int pos = startPos; // pos at which the search starts (continues)
boolean fwdSearch = direction == SearchDirection.FORWARD; // forward search
if (fwdSearch) {
while (pos >= startPos && pos < limitPos) {
int p0; // low bound
int p1; // upper bound
if (pos < gapStart) { // part below gap
p0 = startPos;
p1 = Math.min(gapStart, limitPos);
} else { // part above gap
p0 = Math.max(gapStart, startPos);
p1 = limitPos;
}
//getText(p0, p1 - p0, text);
CharSequence text = snapshot.subSequence(p0, p1);
int textOffset = p0;
pos = finder.find(p0 - textOffset, text,
textOffset, textOffset + text.length(), pos, limitPos);
if (finder.isFound()) {
return new SnapshotPosition(snapshot, pos);
}
}
} else { // backward search limitPos < startPos
pos--; // start one char below the upper bound
while (limitPos <= pos && pos <= startPos) {
int p0; // low bound
int p1; // upper bound
if (pos < gapStart) { // part below gap
p0 = limitPos;
p1 = Math.min(gapStart, startPos);
} else { // part above gap
p0 = Math.max(gapStart, limitPos);
p1 = startPos;
}
//getText(p0, p1 - p0, text);
CharSequence text = snapshot.subSequence(p0, p1);
int textOffset = p0;
pos = finder.find(p0 - textOffset, text,
textOffset, textOffset + text.length(), pos, limitPos);
if (finder.isFound()) {
return new SnapshotPosition(snapshot, pos);
}
}
}
return null; // position outside bounds => not found
}
public enum SearchDirection {
FORWARD,
BACKWARD,
}
public interface SnapshotAdjustFinder {
SnapshotPositionRegion adjustPositionRegion(SnapshotPositionRegion region);
}
public interface SnapshotFinder {
void reset();
int find(int bufferStartPos, CharSequence buffer, int offset1, int offset2, int reqPos, int limitPos);
boolean isFound();
}
private static class SnapshotFinderFactory {
/** Abstract finder implementation. The only <CODE>find()</CODE>
* method must be redefined.
*/
public static abstract class AbstractFinder implements SnapshotFinder {
/** Was the string found? */
protected boolean found;
/** Was the string found? */
@Override
public final boolean isFound() {
return found;
}
/** Reset the finder */
@Override
public void reset() {
found = false;
}
}
/** Generic forward finder that simplifies the search process. */
public static abstract class GenericFwdFinder extends AbstractFinder {
@Override
public final int find(int bufferStartPos, CharSequence buffer,
int offset1, int offset2, int reqPos, int limitPos) {
int offset = reqPos - bufferStartPos;
int limitOffset = limitPos - bufferStartPos - 1;
while (offset >= offset1 && offset < offset2) {
offset += scan(buffer.charAt(offset), (offset == limitOffset));
if (found) {
break;
}
}
return bufferStartPos + offset;
}
/** This function decides if it found a desired string or not.
* The function receives currently searched character and flag if it's
* the last one that is searched or not.
* @return if the function decides that
* it found a desired string it sets <CODE>found = true</CODE> and returns
* how many characters back the searched string begins in forward
* direction (0 stands for current character).
* For example if the function looks for word 'yes' and it gets
* 's' as parameter it sets found = true and returns -2.
* If the string is not yet found it returns how many characters it should go
* in forward direction (in this case it would usually be 1).
* The next searched character will be that one requested.
*/
protected abstract int scan(char ch, boolean lastChar);
}
/** Generic backward finder that simplifies the search process. */
public static abstract class GenericBwdFinder extends AbstractFinder {
@Override
public final int find(int bufferStartPos, CharSequence buffer,
int offset1, int offset2, int reqPos, int limitPos) {
int offset = reqPos - bufferStartPos;
int limitOffset = limitPos - bufferStartPos;
while (offset >= offset1 && offset < offset2) {
offset += scan(buffer.charAt(offset), (offset == limitOffset));
if (found) {
break;
}
}
return bufferStartPos + offset;
}
/** This function decides if it found a desired string or not.
* The function receives currently searched character and flag if it's
* the last one that is searched or not.
* @return if the function decides that
* it found a desired string it sets <CODE>found = true</CODE> and returns
* how many characters back the searched string begins in backward
* direction (0 stands for current character). It is usually 0 as the
* finder usually decides after the last required character but it's
* not always the case e.g. for whole-words-only search it can be 1 or so.
* If the string is not yet found it returns how many characters it should go
* in backward direction (in this case it would usually be -1).
* The next searched character will be that one requested.
*/
protected abstract int scan(char ch, boolean lastChar);
}
/** Next word forward finder */
public static class NextWordFwdFinder extends GenericFwdFinder {
/** Document used to recognize the character types */
BaseDocument doc;
/** Currently inside whitespace */
boolean inWhitespace;
/** Currently inside identifier */
boolean inIdentifier;
/** Currently inside not in word and not in whitespace */
boolean inPunct;
/** Whether scanning the first character */
boolean firstChar;
/** Whether stop on EOL */
boolean stopOnEOL;
/** Stop with successful find on the first white character */
boolean stopOnWhitespace;
public NextWordFwdFinder(BaseDocument doc, boolean stopOnEOL, boolean stopOnWhitespace) {
this.doc = doc;
this.stopOnEOL = stopOnEOL;
this.stopOnWhitespace = stopOnWhitespace;
}
public @Override void reset() {
super.reset();
inWhitespace = false;
inIdentifier = false;
inPunct = false;
firstChar = true;
}
@Override
protected int scan(char ch, boolean lastChar) {
if (stopOnEOL) {
if (ch == '\n') {
found = true;
return firstChar ? 1 : 0;
}
firstChar = false;
}
if (doc.isWhitespace(ch)) { // whitespace char found
if (stopOnWhitespace) {
found = true;
return 0;
} else {
inWhitespace = true;
return 1;
}
}
if (inWhitespace) {
found = true;
return 0;
}
if (inIdentifier) { // inside word
if (doc.isIdentifierPart(ch)) { // still in word
return 1;
}
found = true;
return 0; // found punct
}
if (inPunct) { // inside punctuation
if (doc.isIdentifierPart(ch)) { // a word starts after punct
found = true;
return 0;
}
return 1; // still in punct
}
// just starting - no state assigned yet
if (doc.isIdentifierPart(ch)) {
inIdentifier = true;
return 1;
} else {
inPunct = true;
return 1;
}
}
}
/** Find start of the word. This finder can be used to go to previous
* word or to the start of the current word.
*/
public static class PreviousWordBwdFinder extends GenericBwdFinder {
BaseDocument doc;
/** Currently inside identifier */
boolean inIdentifier;
/** Currently inside not in word and not in whitespace */
boolean inPunct;
/** Stop on EOL */
boolean stopOnEOL;
/** Stop with successful find on the first white character */
boolean stopOnWhitespace;
boolean firstChar;
public PreviousWordBwdFinder(BaseDocument doc, boolean stopOnEOL, boolean stopOnWhitespace) {
this.doc = doc;
this.stopOnEOL = stopOnEOL;
this.stopOnWhitespace = stopOnWhitespace;
}
@Override
public void reset() {
super.reset();
inIdentifier = false;
inPunct = false;
firstChar = true;
}
@Override
protected int scan(char ch, boolean lastChar) {
if (stopOnEOL) {
if (ch == '\n') {
found = true;
return firstChar ? 0 : 1;
}
firstChar = false;
}
if (inIdentifier) { // inside word
if (doc.isIdentifierPart(ch)) {
if (lastChar) {
found = true;
return 0;
}
return -1;
}
found = true;
return 1; // found punct or whitespace
}
if (inPunct) { // inside punctuation
if (doc.isIdentifierPart(ch) || doc.isWhitespace(ch) || lastChar) {
found = true;
return 1;
}
return -1; // still in punct
}
if (doc.isWhitespace(ch)) {
if (stopOnWhitespace) {
found = true;
return 1;
}
return -1;
}
if (doc.isIdentifierPart(ch)) {
inIdentifier = true;
if (lastChar) {
found = true;
return 0;
}
return -1;
}
inPunct = true;
return -1;
}
}
}
}