/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
*
* Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common
* Development and Distribution License("CDDL") (collectively, the
* "License"). You may not use this file except in compliance with the
* License. You can obtain a copy of the License at
* http://www.netbeans.org/cddl-gplv2.html
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
* specific language governing permissions and limitations under the
* License. When distributing the software, include this License Header
* Notice in each file and include the License file at
* nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the
* License Header, with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* Contributor(s):
*
* The Original Software is NetBeans. The Initial Developer of the Original
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun
* Microsystems, Inc. All Rights Reserved.
*
* If you wish your version of this file to be governed by only the CDDL
* or only the GPL Version 2, indicate your decision by adding
* "[Contributor] elects to include this software in this distribution
* under the [CDDL or GPL Version 2] license." If you do not indicate a
* single choice of license, a recipient has the option to distribute
* your version of this file under either the CDDL, the GPL Version 2 or
* to extend the choice of license to its licensees as provided above.
* However, if you add GPL Version 2 code and therefore, elected the GPL
* Version 2 license, then the option applies only if the new code is
* made subject to such option by the copyright holder.
*/
package org.netbeans.modules.ruby.lexer;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenId;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.api.ruby.platform.RubyInstallation;
import org.netbeans.editor.BaseDocument;
import org.netbeans.editor.Utilities;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.csl.spi.GsfUtilities;
import org.netbeans.modules.parsing.api.Source;
import org.netbeans.modules.parsing.spi.Parser;
import org.netbeans.modules.ruby.RubyParseResult;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataObject;
import org.openide.util.Exceptions;
/**
* Utilities associated with lexing or analyzing the document at the
* lexical level, unlike AstUtilities which is contains utilities
* to analyze parsed information about a document.
*
* @author Tor Norbye
*/
public class LexUtilities {
/** Tokens that match a corresponding END statement. Even though while, unless etc.
* can be statement modifiers, those luckily have different token ids so are not a problem
* here.
*/
private static final Set<TokenId> END_PAIRS = new HashSet<TokenId>();
/**
* Tokens that should cause indentation of the next line. This is true for all {@link #END_PAIRS},
* but also includes tokens like "else" that are not themselves matched with end but also contribute
* structure for indentation.
*
*/
private static final Set<TokenId> INDENT_WORDS = new HashSet<TokenId>();
static {
END_PAIRS.add(RubyTokenId.BEGIN);
END_PAIRS.add(RubyTokenId.FOR);
END_PAIRS.add(RubyTokenId.CLASS);
END_PAIRS.add(RubyTokenId.DEF);
END_PAIRS.add(RubyTokenId.DO);
END_PAIRS.add(RubyTokenId.WHILE);
END_PAIRS.add(RubyTokenId.IF);
END_PAIRS.add(RubyTokenId.CLASS);
END_PAIRS.add(RubyTokenId.MODULE);
END_PAIRS.add(RubyTokenId.CASE);
END_PAIRS.add(RubyTokenId.LOOP);
END_PAIRS.add(RubyTokenId.UNTIL);
END_PAIRS.add(RubyTokenId.UNLESS);
INDENT_WORDS.addAll(END_PAIRS);
// Add words that are not matched themselves with an "end",
// but which also provide block structure to indented content
// (usually part of a multi-keyword structure such as if-then-elsif-else-end
// where only the "if" is considered an end-pair.)
INDENT_WORDS.add(RubyTokenId.ELSE);
INDENT_WORDS.add(RubyTokenId.ELSIF);
INDENT_WORDS.add(RubyTokenId.ENSURE);
INDENT_WORDS.add(RubyTokenId.WHEN);
INDENT_WORDS.add(RubyTokenId.RESCUE);
// XXX What about BEGIN{} and END{} ?
}
private LexUtilities() {
}
@CheckForNull
public static BaseDocument getDocument(RubyParseResult result, boolean forceOpen) {
if (result != null) {
Source source = result.getSnapshot().getSource();
return GsfUtilities.getDocument(source.getFileObject(), forceOpen);
}
return null;
}
public static int getLexerOffset(Parser.Result result, int astOffset) {
return result.getSnapshot().getOriginalOffset(astOffset);
}
public static OffsetRange getLexerOffsets(Parser.Result result, OffsetRange astRange) {
int rangeStart = astRange.getStart();
int start = result.getSnapshot().getOriginalOffset(rangeStart);
if (start == rangeStart) {
return astRange;
} else if (start == -1) {
return OffsetRange.NONE;
} else {
// Assumes the translated range maintains size
return new OffsetRange(start, start + astRange.getLength());
}
}
/** Find the ruby token sequence (in case it's embedded in something else at the top level */
@SuppressWarnings("unchecked")
public static TokenSequence<?extends RubyTokenId> getRubyTokenSequence(BaseDocument doc, int offset) {
TokenHierarchy<Document> th = TokenHierarchy.get((Document)doc);
return getRubyTokenSequence(th, offset);
}
@SuppressWarnings("unchecked")
private static TokenSequence<? extends RubyTokenId> findRhtmlDelimited(TokenSequence t, int offset) {
String mimeType = t.language().mimeType();
if (mimeType.equals(RubyInstallation.RHTML_MIME_TYPE) || mimeType.equals(RubyInstallation.YAML_MIME_TYPE)) {
t.move(offset);
if (t.moveNext() && t.token() != null &&
"ruby-delimiter".equals(t.token().id().primaryCategory())) { // NOI18N
// It's a delimiter - move ahead and see if we find it
if (t.moveNext() && t.token() != null &&
"ruby".equals(t.token().id().primaryCategory())) { // NOI18N
TokenSequence<?> ets = t.embedded();
if (ets != null) {
return (TokenSequence<? extends RubyTokenId>)ets;
}
}
}
}
return null;
}
@SuppressWarnings("unchecked")
public static TokenSequence<?extends RubyTokenId> getRubyTokenSequence(TokenHierarchy<Document> th, int offset) {
TokenSequence<?extends RubyTokenId> ts = th.tokenSequence(RubyTokenId.language());
if (ts == null) {
// Possibly an embedding scenario such as an RHTML file
// First try with backward bias true
List<TokenSequence<?>> list = th.embeddedTokenSequences(offset, true);
for (TokenSequence t : list) {
if (t.language() == RubyTokenId.language()) {
ts = t;
break;
} else {
TokenSequence<? extends RubyTokenId> ets = findRhtmlDelimited(t, offset);
if (ets != null) {
return ets;
}
}
}
if (ts == null) {
list = th.embeddedTokenSequences(offset, false);
for (TokenSequence t : list) {
if (t.language() == RubyTokenId.language()) {
ts = t;
break;
} else {
TokenSequence<? extends RubyTokenId> ets = findRhtmlDelimited(t, offset);
if (ets != null) {
return ets;
}
}
}
}
}
return ts;
}
public static TokenSequence<?extends RubyTokenId> getPositionedSequence(BaseDocument doc, int offset) {
TokenSequence<?extends RubyTokenId> ts = getRubyTokenSequence(doc, offset);
if (ts != null) {
try {
ts.move(offset);
} catch (AssertionError e) {
DataObject dobj = (DataObject)doc.getProperty(Document.StreamDescriptionProperty);
if (dobj != null) {
Exceptions.attachMessage(e, FileUtil.getFileDisplayName(dobj.getPrimaryFile()));
}
throw e;
}
if (!ts.moveNext() && !ts.movePrevious()) {
return null;
}
return ts;
}
return null;
}
public static Token<?extends RubyTokenId> getToken(BaseDocument doc, int offset) {
TokenSequence<?extends RubyTokenId> ts = getPositionedSequence(doc, offset);
if (ts != null) {
return ts.token();
}
return null;
}
public static char getTokenChar(BaseDocument doc, int offset) {
Token<?extends RubyTokenId> token = getToken(doc, offset);
if (token != null) {
String text = token.text().toString();
if (text.length() > 0) { // Usually true, but I could have gotten EOF right?
return text.charAt(0);
}
}
return 0;
}
/** Search forwards in the token sequence until a token of type <code>down</code> is found */
public static OffsetRange findHeredocEnd(TokenSequence<?extends RubyTokenId> ts, Token<?extends RubyTokenId> startToken) {
// Look for the end of the given heredoc
String text = startToken.text().toString();
assert text.startsWith("<<");
text = text.substring(2);
if (text.startsWith("-")) {
text = text.substring(1);
}
if ((text.startsWith("\"") && text.endsWith("\"")) || (text.startsWith("'") && text.endsWith("'"))) {
text = text.substring(0, text.length()-2);
}
String textn = text+"\n";
while (ts.moveNext()) {
Token<?extends RubyTokenId> token = ts.token();
TokenId id = token.id();
if (id == RubyTokenId.STRING_END || id == RubyTokenId.QUOTED_STRING_END) {
String t = token.text().toString();
if (text.equals(t) || textn.equals(t)) {
return new OffsetRange(ts.offset(), ts.offset() + token.length());
}
}
}
return OffsetRange.NONE;
}
/** Search forwards in the token sequence until a token of type <code>down</code> is found */
public static OffsetRange findHeredocBegin(TokenSequence<?extends RubyTokenId> ts, Token<?extends RubyTokenId> endToken) {
// Look for the end of the given heredoc
String text = endToken.text().toString();
if (text.endsWith("\n")) {
text = text.substring(0, text.length()-1);
}
String textQuotes = "\"" + text + "\"";
String textSQuotes = "'" + text + "'";
while (ts.movePrevious()) {
Token<?extends RubyTokenId> token = ts.token();
TokenId id = token.id();
if (id == RubyTokenId.STRING_BEGIN || id == RubyTokenId.QUOTED_STRING_BEGIN) {
String t = token.text().toString();
String marker = null;
if (t.startsWith("<<-")) {
marker = t.substring(3);
} else if (t.startsWith("<<")) {
marker = t.substring(2);
}
if (marker != null && (text.equals(marker) || textQuotes.equals(marker) || textSQuotes.equals(marker))) {
return new OffsetRange(ts.offset(), ts.offset() + token.length());
}
}
}
return OffsetRange.NONE;
}
/** Search forwards in the token sequence until a token of type <code>down</code> is found */
public static OffsetRange findFwd(BaseDocument doc, TokenSequence<?extends RubyTokenId> ts, TokenId up,
TokenId down) {
int balance = 0;
while (ts.moveNext()) {
Token<?extends RubyTokenId> token = ts.token();
TokenId id = token.id();
if (id == up) {
balance++;
} else if (id == down) {
if (balance == 0) {
return new OffsetRange(ts.offset(), ts.offset() + token.length());
}
balance--;
}
}
return OffsetRange.NONE;
}
/** Search backwards in the token sequence until a token of type <code>up</code> is found */
public static OffsetRange findBwd(BaseDocument doc, TokenSequence<?extends RubyTokenId> ts, TokenId up,
TokenId down) {
int balance = 0;
while (ts.movePrevious()) {
Token<?extends RubyTokenId> token = ts.token();
TokenId id = token.id();
if (id == up) {
if (balance == 0) {
return new OffsetRange(ts.offset(), ts.offset() + token.length());
}
balance++;
} else if (id == down) {
balance--;
}
}
return OffsetRange.NONE;
}
/** Find the token that begins a block terminated by "end". This is a token
* in the END_PAIRS array. Walk backwards and find the corresponding token.
* It does not use indentation for clues since this could be wrong and be
* precisely the reason why the user is using pair matching to see what's wrong.
*/
public static OffsetRange findBegin(BaseDocument doc, TokenSequence<?extends RubyTokenId> ts) {
int balance = 0;
while (ts.movePrevious()) {
Token<?extends RubyTokenId> token = ts.token();
TokenId id = token.id();
if (isBeginToken(id, doc, ts)) {
// No matching dot for "do" used in conditionals etc.)) {
if (balance == 0) {
return new OffsetRange(ts.offset(), ts.offset() + token.length());
}
balance--;
} else if (id == RubyTokenId.END) {
balance++;
}
}
return OffsetRange.NONE;
}
public static OffsetRange findEnd(BaseDocument doc, TokenSequence<?extends RubyTokenId> ts) {
int balance = 0;
while (ts.moveNext()) {
Token<?extends RubyTokenId> token = ts.token();
TokenId id = token.id();
if (isBeginToken(id, doc, ts)) {
balance--;
} else if (id == RubyTokenId.END) {
if (balance == 0) {
return new OffsetRange(ts.offset(), ts.offset() + token.length());
}
balance++;
}
}
return OffsetRange.NONE;
}
/** Determine whether "do" is an indent-token (e.g. matches an end) or if
* it's simply a separator in while,until,for expressions)
*/
public static boolean isEndmatchingDo(BaseDocument doc, int offset) {
// In the following case, do is dominant:
// expression.do
// whatever
// end
//
// However, not here:
// while true do
// whatever
// end
//
// In the second case, the end matches the while, but in the first case
// the end matches the do
// Look at the first token of the current line
try {
int first = Utilities.getRowFirstNonWhite(doc, offset);
if (first != -1) {
Token<? extends RubyTokenId> token = getToken(doc, first);
if (token != null) {
TokenId id = token.id();
if (id == RubyTokenId.WHILE || id == RubyTokenId.UNTIL || id == RubyTokenId.FOR) {
return false;
}
}
}
} catch (BadLocationException ble) {
// do nothing - see #154991
}
return true;
}
/**
* Return true iff the given token is a token that should be matched
* with a corresponding "end" token, such as "begin", "def", "module",
* etc.
*/
public static boolean isBeginToken(TokenId id, BaseDocument doc, int offset) {
if (id == RubyTokenId.DO) {
return isEndmatchingDo(doc, offset);
}
return END_PAIRS.contains(id);
}
/**
* Return true iff the given token is a token that should be matched
* with a corresponding "end" token, such as "begin", "def", "module",
* etc.
*/
public static boolean isBeginToken(TokenId id, BaseDocument doc, TokenSequence<?extends RubyTokenId> ts) {
if (id == RubyTokenId.DO) {
return isEndmatchingDo(doc, ts.offset());
}
return END_PAIRS.contains(id);
}
/**
* Return true iff the given token is a token that indents its content,
* such as the various begin tokens as well as "else", "when", etc.
*/
public static boolean isIndentToken(TokenId id) {
return INDENT_WORDS.contains(id);
}
/** Compute the balance of begin/end tokens on the line.
* @param doc the document
* @param offset The offset somewhere on the line
* @param upToOffset If true, only compute the line balance up to the given offset (inclusive),
* and if false compute the balance for the whole line
*/
public static int getBeginEndLineBalance(BaseDocument doc, int offset, boolean upToOffset) {
try {
int begin = Utilities.getRowStart(doc, offset);
int end = upToOffset ? offset : Utilities.getRowEnd(doc, offset);
TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, begin);
if (ts == null) {
return 0;
}
ts.move(begin);
if (!ts.moveNext()) {
return 0;
}
int balance = 0;
do {
Token<?extends RubyTokenId> token = ts.token();
TokenId id = token.id();
if (isBeginToken(id, doc, ts)) {
balance++;
} else if (id == RubyTokenId.END) {
balance--;
}
} while (ts.moveNext() && (ts.offset() <= end));
return balance;
} catch (BadLocationException ble) {
return 0;
}
}
/** Compute the balance of begin/end tokens on the line */
public static int getLineBalance(BaseDocument doc, int offset, TokenId up, TokenId down) {
try {
int begin = Utilities.getRowStart(doc, offset);
int end = Utilities.getRowEnd(doc, offset);
TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, begin);
if (ts == null) {
return 0;
}
ts.move(begin);
if (!ts.moveNext()) {
return 0;
}
int balance = 0;
do {
Token<?extends RubyTokenId> token = ts.token();
TokenId id = token.id();
if (id == up) {
balance++;
} else if (id == down) {
balance--;
}
} while (ts.moveNext() && (ts.offset() <= end));
return balance;
} catch (BadLocationException ble) {
return 0;
}
}
/**
* The same as braceBalance but generalized to any pair of matching
* tokens.
* @param open the token that increses the count
* @param close the token that decreses the count
*/
public static int getTokenBalance(BaseDocument doc, TokenId open, TokenId close, int offset)
throws BadLocationException {
TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, 0);
if (ts == null) {
return 0;
}
// XXX Why 0? Why not offset?
ts.moveIndex(0);
if (!ts.moveNext()) {
return 0;
}
int balance = 0;
do {
Token t = ts.token();
if (t.id() == open) {
balance++;
} else if (t.id() == close) {
balance--;
}
} while (ts.moveNext());
return balance;
}
/**
* Return true iff the line for the given offset is a Ruby comment line.
* This will return false for lines that contain comments (even when the
* offset is within the comment portion) but also contain code.
*/
public static boolean isCommentOnlyLine(BaseDocument doc, int offset)
throws BadLocationException {
int begin = Utilities.getRowFirstNonWhite(doc, offset);
if (begin == -1) {
return false; // whitespace only
}
if (begin == doc.getLength()) {
return false;
}
return doc.getText(begin, 1).equals("#");
}
/**
* Return the string at the given position, or null if none
*/
@SuppressWarnings("unchecked")
public static String getStringAt(int caretOffset, TokenHierarchy<Document> th) {
TokenSequence<?extends RubyTokenId> ts = getRubyTokenSequence(th, caretOffset);
if (ts == null) {
return null;
}
ts.move(caretOffset);
if (!ts.moveNext() && !ts.movePrevious()) {
return null;
}
if (ts.offset() == caretOffset) {
// We're looking at the offset to the RIGHT of the caret
// and here I care about what's on the left
if (!ts.movePrevious()) {
return null;
}
}
Token<?extends RubyTokenId> token = ts.token();
if (token != null) {
TokenId id = token.id();
// We're within a String that has embedded Ruby. Drop into the
// embedded language and see if we're within a literal string there.
if (id == RubyTokenId.EMBEDDED_RUBY) {
ts = (TokenSequence)ts.embedded();
assert ts != null;
ts.move(caretOffset);
if (!ts.moveNext() && !ts.movePrevious()) {
return null;
}
token = ts.token();
id = token.id();
}
String string = null;
// Skip over embedded Ruby segments and literal strings until you find the beginning
int segments = 0;
while ((id == RubyTokenId.ERROR) || (id == RubyTokenId.STRING_LITERAL) ||
(id == RubyTokenId.QUOTED_STRING_LITERAL) || (id == RubyTokenId.EMBEDDED_RUBY)) {
string = token.text().toString();
segments++;
if (!ts.movePrevious()) {
return null;
}
token = ts.token();
id = token.id();
}
if ((id == RubyTokenId.STRING_BEGIN) || (id == RubyTokenId.QUOTED_STRING_BEGIN)) {
if (segments == 1) {
return string;
} else {
// Build up the String from the sequence
StringBuilder sb = new StringBuilder();
while (ts.moveNext()) {
token = ts.token();
id = token.id();
if ((id == RubyTokenId.ERROR) || (id == RubyTokenId.STRING_LITERAL) ||
(id == RubyTokenId.QUOTED_STRING_LITERAL) ||
(id == RubyTokenId.EMBEDDED_RUBY)) {
sb.append(token.text());
} else {
break;
}
}
return sb.toString();
}
}
}
return null;
}
/**
* Check if the caret is inside a literal string that is associated with
* a require statement.
*
* @return The offset of the beginning of the require string, or -1
* if the offset is not inside a require string.
*/
public static int getRequireStringOffset(int caretOffset, TokenHierarchy<Document> th) {
TokenEvaluator evaluator = new TokenEvaluator() {
@Override
boolean next() {
return false;
}
@Override
boolean handled() {
return true;
}
@Override
int returnValue() {
if (this.token.id() == RubyTokenId.IDENTIFIER) {
String text = token.text().toString();
if (text.equals("require") || text.equals("load")) {
return start;
} else {
return -1;
}
}
return -1;
}
};
return getStringOffset(caretOffset, th, evaluator);
}
/**
* Check if the caret is inside a literal string that is associated with
* a :class or :class_name symbol.
*
* @return The offset of the beginning of the class name string, or -1
* if the offset is not inside a class name string.
*/
public static int getClassNameStringOffset(int caretOffset, TokenHierarchy<Document> th) {
TokenEvaluator evaluator = new TokenEvaluator() {
@Override
boolean next() {
return token.id() == RubyTokenId.NONUNARY_OP;
}
@Override
boolean handled() {
return true;
}
@Override
int returnValue() {
if (this.token.id() == RubyTokenId.TYPE_SYMBOL) {
String text = token.text().toString();
if (text.equals("class") || text.equals("class_name")) {
return start;
} else {
return -1;
}
}
return -1;
}
};
return getStringOffset(caretOffset, th, evaluator);
}
private static int getStringOffset(int caretOffset, TokenHierarchy<Document> th, TokenEvaluator evaluator) {
TokenSequence<?extends RubyTokenId> ts = getRubyTokenSequence(th, caretOffset);
if (ts == null) {
return -1;
}
ts.move(caretOffset);
if (!ts.moveNext() && !ts.movePrevious()) {
return -1;
}
if (ts.offset() == caretOffset) {
// We're looking at the offset to the RIGHT of the caret
// and here I care about what's on the left
if (!ts.movePrevious()) {
return -1;
}
}
Token<?extends RubyTokenId> token = ts.token();
if (token != null) {
TokenId id = token.id();
// Skip over embedded Ruby segments and literal strings until you find the beginning
while ((id == RubyTokenId.ERROR) || (id == RubyTokenId.STRING_LITERAL) ||
(id == RubyTokenId.QUOTED_STRING_LITERAL) || (id == RubyTokenId.EMBEDDED_RUBY)) {
if (!ts.movePrevious()) {
return -1;
}
token = ts.token();
id = token.id();
}
int stringStart = ts.offset() + token.length();
evaluator.setStart(stringStart);
if ((id == RubyTokenId.STRING_BEGIN) || (id == RubyTokenId.QUOTED_STRING_BEGIN)) {
// Completion of literal strings within require calls
while (ts.movePrevious()) {
token = ts.token();
id = token.id();
if ((id == RubyTokenId.WHITESPACE) || (id == RubyTokenId.LPAREN) ||
(id == RubyTokenId.STRING_LITERAL) ||
(id == RubyTokenId.QUOTED_STRING_LITERAL)) {
continue;
}
evaluator.setToken(token);
if (evaluator.next()) {
continue;
}
if (evaluator.handled()) {
return evaluator.returnValue();
}
}
}
}
return -1;
}
private static abstract class TokenEvaluator {
protected Token token;
protected int start;
void setToken(Token token) {
this.token = token;
}
void setStart(int start) {
this.start = start;
}
abstract boolean next();
abstract boolean handled();
abstract int returnValue();
}
public static int getSingleQuotedStringOffset(int caretOffset, TokenHierarchy<Document> th) {
return getLiteralStringOffset(caretOffset, th, RubyTokenId.STRING_BEGIN);
}
public static int getDoubleQuotedStringOffset(int caretOffset, TokenHierarchy<Document> th) {
return getLiteralStringOffset(caretOffset, th, RubyTokenId.QUOTED_STRING_BEGIN);
}
public static int getRegexpOffset(int caretOffset, TokenHierarchy<Document> th) {
return getLiteralStringOffset(caretOffset, th, RubyTokenId.REGEXP_BEGIN);
}
/**
* Determine if the caret is inside a literal string, and if so, return its starting
* offset. Return -1 otherwise.
*/
@SuppressWarnings("unchecked")
private static int getLiteralStringOffset(int caretOffset, TokenHierarchy<Document> th,
RubyTokenId begin) {
TokenSequence<?extends RubyTokenId> ts = getRubyTokenSequence(th, caretOffset);
if (ts == null) {
return -1;
}
ts.move(caretOffset);
if (!ts.moveNext() && !ts.movePrevious()) {
return -1;
}
if (ts.offset() == caretOffset) {
// We're looking at the offset to the RIGHT of the caret
// and here I care about what's on the left
if (!ts.movePrevious()) {
return -1;
}
}
Token<?extends RubyTokenId> token = ts.token();
if (token != null) {
TokenId id = token.id();
// We're within a String that has embedded Ruby. Drop into the
// embedded language and see if we're within a literal string there.
if (id == RubyTokenId.EMBEDDED_RUBY) {
ts = (TokenSequence)ts.embedded();
assert ts != null;
ts.move(caretOffset);
if (!ts.moveNext() && !ts.movePrevious()) {
return -1;
}
token = ts.token();
id = token.id();
}
// Skip over embedded Ruby segments and literal strings until you find the beginning
while ((id == RubyTokenId.ERROR) || (id == RubyTokenId.STRING_LITERAL) ||
(id == RubyTokenId.QUOTED_STRING_LITERAL) ||
(id == RubyTokenId.REGEXP_LITERAL) || (id == RubyTokenId.EMBEDDED_RUBY)) {
if (!ts.movePrevious()) {
return -1;
}
token = ts.token();
id = token.id();
}
if (id == begin) {
if (!ts.moveNext()) {
return -1;
}
return ts.offset();
}
}
return -1;
}
public static boolean isInsideQuotedString(BaseDocument doc, int offset) {
TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, offset);
if (ts == null) {
return false;
}
ts.move(offset);
if (ts.moveNext()) {
Token<?extends RubyTokenId> token = ts.token();
TokenId id = token.id();
if (id == RubyTokenId.QUOTED_STRING_LITERAL || id == RubyTokenId.QUOTED_STRING_END) {
return true;
}
}
if (ts.movePrevious()) {
Token<?extends RubyTokenId> token = ts.token();
TokenId id = token.id();
if (id == RubyTokenId.QUOTED_STRING_LITERAL || id == RubyTokenId.QUOTED_STRING_BEGIN) {
return true;
}
}
return false;
}
public static boolean isInsideRegexp(BaseDocument doc, int offset) {
TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, offset);
if (ts == null) {
return false;
}
ts.move(offset);
if (ts.moveNext()) {
Token<?extends RubyTokenId> token = ts.token();
TokenId id = token.id();
if (id == RubyTokenId.REGEXP_LITERAL || id == RubyTokenId.REGEXP_END) {
return true;
}
}
if (ts.movePrevious()) {
Token<?extends RubyTokenId> token = ts.token();
TokenId id = token.id();
if (id == RubyTokenId.REGEXP_LITERAL || id == RubyTokenId.REGEXP_BEGIN) {
return true;
}
}
return false;
}
public static OffsetRange getCommentBlock(BaseDocument doc, int caretOffset) {
// Check if the caret is within a comment, and if so insert a new
// leaf "node" which contains the comment line and then comment block
try {
Token<?extends RubyTokenId> token = LexUtilities.getToken(doc, caretOffset);
if ((token != null) && (token.id() == RubyTokenId.LINE_COMMENT)) {
// First add a range for the current line
int begin = Utilities.getRowStart(doc, caretOffset);
int end = Utilities.getRowEnd(doc, caretOffset);
if (LexUtilities.isCommentOnlyLine(doc, caretOffset)) {
while (begin > 0) {
int newBegin = Utilities.getRowStart(doc, begin - 1);
if ((newBegin < 0) || !LexUtilities.isCommentOnlyLine(doc, newBegin)) {
begin = Utilities.getRowFirstNonWhite(doc, begin);
break;
}
begin = newBegin;
}
int length = doc.getLength();
while (true) {
int newEnd = Utilities.getRowEnd(doc, end + 1);
if ((newEnd >= length) || !LexUtilities.isCommentOnlyLine(doc, newEnd)) {
end = Utilities.getRowLastNonWhite(doc, end)+1;
break;
}
end = newEnd;
}
if (begin < end) {
return new OffsetRange(begin, end);
}
} else {
// It's just a line comment next to some code
TokenHierarchy<Document> th = TokenHierarchy.get((Document)doc);
int offset = token.offset(th);
return new OffsetRange(offset, offset + token.length());
}
} else if (token != null && token.id() == RubyTokenId.DOCUMENTATION) {
// Select the whole token block
TokenHierarchy<BaseDocument> th = TokenHierarchy.get(doc);
int begin = token.offset(th);
int end = begin + token.length();
return new OffsetRange(begin, end);
}
} catch (BadLocationException ble) {
// do nothing - see #154991;
}
return OffsetRange.NONE;
}
/**
* Back up to the first space character prior to the given offset - as long as
* it's on the same line! If there's only leading whitespace on the line up
* to the lex offset, return the offset itself
*/
public static int findSpaceBegin(BaseDocument doc, int lexOffset) {
TokenSequence ts = LexUtilities.getRubyTokenSequence(doc, lexOffset);
if (ts == null) {
return lexOffset;
}
boolean allowPrevLine = false;
int lineStart;
try {
lineStart = Utilities.getRowStart(doc, Math.min(lexOffset, doc.getLength()));
int prevLast = lineStart-1;
if (lineStart > 0) {
prevLast = Utilities.getRowLastNonWhite(doc, lineStart-1);
if (prevLast != -1) {
char c = doc.getText(prevLast, 1).charAt(0);
if (c == ',') {
// Arglist continuation? // TODO : check lexing
allowPrevLine = true;
}
}
}
if (!allowPrevLine) {
int firstNonWhite = Utilities.getRowFirstNonWhite(doc, lineStart);
if (lexOffset <= firstNonWhite || firstNonWhite == -1) {
return lexOffset;
}
} else {
// Make lineStart so small that Math.max won't cause any problems
int firstNonWhite = Utilities.getRowFirstNonWhite(doc, lineStart);
if (prevLast >= 0 && (lexOffset <= firstNonWhite || firstNonWhite == -1)) {
return prevLast+1;
}
lineStart = 0;
}
} catch (BadLocationException ble) {
return lexOffset;
}
ts.move(lexOffset);
if (ts.moveNext()) {
if (lexOffset > ts.offset()) {
// We're in the middle of a token
return Math.max((ts.token().id() == RubyTokenId.WHITESPACE) ?
ts.offset() : lexOffset, lineStart);
}
while (ts.movePrevious()) {
Token token = ts.token();
if (token.id() != RubyTokenId.WHITESPACE) {
return Math.max(ts.offset() + token.length(), lineStart);
}
}
}
return lexOffset;
}
/**
* Get the rdoc documentation associated with the given node in the given document.
* The node must have position information that matches the source in the document.
*/
public static OffsetRange findRDocRange(BaseDocument baseDoc, int methodBegin) {
int begin = methodBegin;
try {
if (methodBegin >= baseDoc.getLength()) {
return OffsetRange.NONE;
}
// Search to previous lines, locate comments. Once we have a non-whitespace line that isn't
// a comment, we're done
int offset = Utilities.getRowStart(baseDoc, methodBegin);
offset--;
// Skip empty and whitespace lines
while (offset >= 0) {
// Find beginning of line
offset = Utilities.getRowStart(baseDoc, offset);
if (!Utilities.isRowEmpty(baseDoc, offset) &&
!Utilities.isRowWhite(baseDoc, offset)) {
break;
}
offset--;
}
if (offset < 0) {
return OffsetRange.NONE;
}
while (offset >= 0) {
// Find beginning of line
offset = Utilities.getRowStart(baseDoc, offset);
if (Utilities.isRowEmpty(baseDoc, offset) || Utilities.isRowWhite(baseDoc, offset)) {
// Empty lines not allowed within an rdoc
break;
}
// This is a comment line we should include
int lineBegin = Utilities.getRowFirstNonWhite(baseDoc, offset);
int lineEnd = Utilities.getRowLastNonWhite(baseDoc, offset) + 1;
String line = baseDoc.getText(lineBegin, lineEnd - lineBegin);
// Tolerate "public", "private" and "protected" here --
// Test::Unit::Assertions likes to put these in front of each
// method.
if (line.startsWith("#")) {
begin = lineBegin;
} else if (line.startsWith("=end") &&
(lineBegin == Utilities.getRowStart(baseDoc, offset))) {
// It could be a =begin,=end document - see scanf.rb in Ruby lib for example. Treat this differently.
int docBegin = findInlineDocStart(baseDoc, offset);
if (docBegin != -1) {
begin = docBegin;
} else {
return OffsetRange.NONE;
}
} else if (line.equals("public") || line.equals("private") ||
line.equals("protected")) { // NOI18N
// Skip newlines back up to the comment
offset--;
while (offset >= 0) {
// Find beginning of line
offset = Utilities.getRowStart(baseDoc, offset);
if (!Utilities.isRowEmpty(baseDoc, offset) &&
!Utilities.isRowWhite(baseDoc, offset)) {
break;
}
offset--;
}
continue;
} else {
// No longer in a comment
break;
}
// Previous line
offset--;
}
} catch (BadLocationException ble) {
// do nothing - see #154991
}
if (methodBegin > begin) {
return new OffsetRange(begin, methodBegin);
} else {
return OffsetRange.NONE;
}
}
private static int findInlineDocStart(BaseDocument baseDoc, int offset) throws BadLocationException {
// offset points to a line containing =end
// Skip the =end list
offset = Utilities.getRowStart(baseDoc, offset);
offset--;
// Search backwards in the document for the =begin (if any) and add all lines in reverse
// order in between.
while (offset >= 0) {
// Find beginning of line
offset = Utilities.getRowStart(baseDoc, offset);
// This is a comment line we should include
int lineBegin = offset;
int lineEnd = Utilities.getRowEnd(baseDoc, offset);
String line = baseDoc.getText(lineBegin, lineEnd - lineBegin);
if (line.startsWith("=begin")) {
// We're done!
return lineBegin;
}
// Previous line
offset--;
}
return -1;
}
}