/*
* 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-2006 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;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import org.jrubyparser.ast.CallNode;
import org.jrubyparser.ast.Node;
import org.jrubyparser.ast.NodeType;
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.EditorOptions;
import org.netbeans.modules.csl.api.KeystrokeHandler;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.csl.spi.GsfUtilities;
import org.netbeans.modules.csl.spi.ParserResult;
import org.netbeans.modules.editor.indent.api.IndentUtils;
import org.netbeans.modules.ruby.lexer.LexUtilities;
import org.netbeans.modules.ruby.lexer.RubyTokenId;
import org.openide.util.Exceptions;
/**
* Provide bracket completion for Ruby.
* This class provides three broad services:
* - Identifying matching pairs (parentheses, begin/end pairs etc.), which
* is used both for highlighting in the IDE (when the caret is on for example
* an if statement, the corresponding end token is highlighted), and navigation
* where you can jump between matching pairs.
* - Automatically inserting corresponding pairs when you insert a character.
* For example, if you insert a single quote, a corresponding ending quote
* is inserted - unless you're typing "over" the existing quote (you should
* be able to type foo = "hello" without having to arrow over the second
* quote that was inserted after you typed the first one).
* - Automatically adjusting indentation in some scenarios, for example
* when you type the final "d" in "end" - and readjusting it back to the
* original indentation if you continue typing something other than "end",
* e.g. "endian".
*
* The logic around inserting matching ""'s is heavily based on the Java editor
* implementation, and probably should be rewritten to be Ruby oriented.
* One thing they did is process the characters BEFORE the character has been
* inserted into the document. This has some advantages - it's easy to detect
* whether you're typing in the middle of a string since the token hierarchy
* has not been updated yet. On the other hand, it makes it hard to identify
* whether some characters are what we expect - is a "/" truly a regexp starter
* or something else? The Ruby lexer has lots of logic and state to determine
* this. I think it would be better to switch to after-insert logic for this.
*
* @todo Match braces within literal strings, as in #{}
* @todo Match || in the argument list of blocks? do { |foo| etc. }
* @todo I'm currently highlighting the indentation tokens (else, elsif, ensure, etc.)
* by finding the corresponding begin. For "illegal" tokens, e.g. def foo; else; end;
* this means I'll show "def" as the matching token for else, which is wrong.
* I should make the "indentation tokens" list into a map and associate them
* with their corresponding tokens, such that an else is only lined up with an if,
* etc.
* @todo Pressing newline in a parameter list doesn't work well if it's on a blockdefining
* line - e.g. def foo(a,b => it will insert the end BEFORE the closing paren!
* @todo Pressing space in a comment beyond the textline limit should wrap text?
* http://ruby.netbeans.org/issues/show_bug.cgi?id=11553
* @todo Make ast-selection pick up =begin/=end documentation blocks
*
* @author Tor Norbye
*/
public class RubyKeystrokeHandler implements KeystrokeHandler {
/** When true, automatically reflows comments that are being edited according to the rdoc
* conventions as well as the right hand side margin
*/
private static final boolean REFLOW_COMMENTS = Boolean.getBoolean("ruby.autowrap.comments"); // NOI18N
/** When true, continue comments if you press return in a line comment (that does not
* also have code on the same line
*/
//static final boolean CONTINUE_COMMENTS = !Boolean.getBoolean("ruby.no.cont.comment"); // NOI18N
static final boolean CONTINUE_COMMENTS = Boolean.getBoolean("ruby.cont.comment"); // NOI18N
/** Tokens which indicate that we're within a literal string */
private final static TokenId[] STRING_TOKENS = // XXX What about RubyTokenId.STRING_BEGIN or QUOTED_STRING_BEGIN?
{
RubyTokenId.STRING_LITERAL, RubyTokenId.QUOTED_STRING_LITERAL,
RubyTokenId.STRING_END, RubyTokenId.QUOTED_STRING_END
};
/** Tokens which indicate that we're within a regexp string */
// XXX What about RubyTokenId.REGEXP_BEGIN?
private static final TokenId[] REGEXP_TOKENS = { RubyTokenId.REGEXP_LITERAL, RubyTokenId.REGEXP_END };
/** Used in =begin/=end completion */
private final static String EQ_BEGIN = "=begin"; // NOI18N
/** When != -1, this indicates that we previously adjusted the indentation of the
* line to the given offset, and if it turns out that the user changes that token,
* we revert to the original indentation
*/
private int previousAdjustmentOffset = -1;
/** True iff we're processing bracket matching AFTER the key has been inserted rather than before */
private boolean isAfter;
/**
* The indentation to revert to when previousAdjustmentOffset is set and the token
* changed
*/
private int previousAdjustmentIndent;
public RubyKeystrokeHandler() {
}
public boolean isInsertMatchingEnabled(BaseDocument doc) {
// The editor options code is calling methods on BaseOptions instead of looking in the settings map :(
//Boolean b = ((Boolean)Settings.getValue(doc.getKitClass(), SettingsNames.PAIR_CHARACTERS_COMPLETION));
//return b == null || b.booleanValue();
EditorOptions options = EditorOptions.get(RubyInstallation.RUBY_MIME_TYPE);
if (options != null) {
return options.getMatchBrackets();
}
return true;
}
public int beforeBreak(Document document, int offset, JTextComponent target)
throws BadLocationException {
isAfter = false;
Caret caret = target.getCaret();
BaseDocument doc = (BaseDocument)document;
boolean insertMatching = isInsertMatchingEnabled(doc);
int lineBegin = Utilities.getRowStart(doc,offset);
int lineEnd = Utilities.getRowEnd(doc,offset);
if (lineBegin == offset && lineEnd == offset) {
// Pressed return on a blank newline - do nothing
return -1;
}
// Look for an unterminated heredoc string
if (lineBegin != -1 && lineEnd != -1) {
TokenSequence<?extends RubyTokenId> lineTs = LexUtilities.getRubyTokenSequence(doc, offset);
if (lineTs != null) {
lineTs.move(lineBegin);
StringBuilder sb = new StringBuilder();
while (lineTs.moveNext() && lineTs.offset() <= lineEnd) {
Token<?extends RubyTokenId> token = lineTs.token();
TokenId id = token.id();
if (id == RubyTokenId.STRING_BEGIN) {
String text = token.text().toString();
if (text.startsWith("<<") && insertMatching) {
StringBuilder markerBuilder = new StringBuilder();
for (int i = 2, n = text.length(); i < n; i++) {
char c = text.charAt(i);
if ((c == '\n') || (c == '\r')) {
break;
}
markerBuilder.append(c);
}
String marker = markerBuilder.toString();
// Handle indented heredoc
if (marker.startsWith("-")) {
marker = marker.substring(1);
}
if ((marker.startsWith("'") && marker.endsWith("'")) ||
((marker.startsWith("\"") && marker.endsWith("\"")))){
marker = marker.substring(1, marker.length()-2);
}
// Next token should be string contents or a string end marker
//boolean addEndMarker = true;
TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, offset);
ts.move(offset);
// XXX No, this is bogus, find a better way to detect whether the string is matched,
// perhaps using "find matching?"
OffsetRange range = LexUtilities.findHeredocEnd(ts, token);
if (range == OffsetRange.NONE) {
sb.append("\n");
sb.append(marker);
//sb.append("\n");
}
}
}
}
if (sb.length() > 0) {
if (lineEnd == doc.getLength()) {
// At the end of the buffer we need a newline after the end
// marker. On other lines, we don't.
sb.append("\n");
}
doc.insertString(lineEnd, sb.toString(), null);
caret.setDot(lineEnd);
return -1;
}
}
}
TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, offset);
if (ts == null) {
return -1;
}
ts.move(offset);
if (!ts.moveNext() && !ts.movePrevious()) {
return -1;
}
Token<?extends RubyTokenId> token = ts.token();
TokenId id = token.id();
// Is it an umatched =begin token?
if (insertMatching && (((id == RubyTokenId.ERROR) && (ts.offset() == (offset - 6)) &&
token.text().toString().startsWith(EQ_BEGIN)) ||
(id == RubyTokenId.BEGIN && ts.offset() == Utilities.getRowStart(doc, offset) + 1 &&
EQ_BEGIN.equals(doc.getText(ts.offset() - 1, EQ_BEGIN.length()))) ||
(id == RubyTokenId.NONUNARY_OP && ts.offset() + EQ_BEGIN.length() <= doc.getLength() &&
EQ_BEGIN.equals(doc.getText(ts.offset(), EQ_BEGIN.length()))))) {
// NOI18N
doc.insertString(offset, "\n=end", null); // NOI18N
caret.setDot(offset);
return -1;
}
// Insert an end statement? Insert a } marker?
boolean[] insertEndResult = new boolean[1];
boolean[] insertRBraceResult = new boolean[1];
int[] indentResult = new int[1];
boolean insert = insertMatching &&
isEndMissing(doc, offset, false, insertEndResult, insertRBraceResult, null, indentResult);
if (insert) {
boolean insertEnd = insertEndResult[0];
boolean insertRBrace = insertRBraceResult[0];
int indent = indentResult[0];
int afterLastNonWhite = Utilities.getRowLastNonWhite(doc, offset);
// We've either encountered a further indented line, or a line that doesn't
// look like the end we're after, so insert a matching end.
StringBuilder sb = new StringBuilder();
if (offset > afterLastNonWhite) {
sb.append("\n"); // XXX On Windows, do \r\n?
sb.append(IndentUtils.createIndentString(doc, indent));
} else {
// I'm inserting a newline in the middle of a sentence, such as the scenario in #118656
// I should insert the end AFTER the text on the line
String restOfLine = doc.getText(offset, Utilities.getRowEnd(doc, afterLastNonWhite)-offset);
sb.append(restOfLine);
sb.append("\n");
sb.append(IndentUtils.createIndentString(doc, indent));
doc.remove(offset, restOfLine.length());
}
if (insertEnd) {
sb.append("end"); // NOI18N
} else {
assert insertRBrace;
sb.append("}"); // NOI18N
}
int insertOffset = offset;
doc.insertString(insertOffset, sb.toString(), null);
caret.setDot(insertOffset);
return -1;
}
// Special case: since I do hash completion, if you try to type
// y = Thread.start {
// code here
// }
// you end up with
// y = Thread.start {|}
// If you hit newline at this point, you end up with
// y = Thread.start {
// |}
// which is not as helpful as it would be if we were not doing hash-matching
// (in that case we'd notice the brace imbalance, and insert the closing
// brace on the line below the insert position, and indent properly.
// Catch this scenario and handle it properly.
if ((id == RubyTokenId.RBRACE || id == RubyTokenId.RBRACKET) && (Utilities.getRowLastNonWhite(doc, offset) == offset)) {
int indent = GsfUtilities.getLineIndent(doc, offset);
StringBuilder sb = new StringBuilder();
// XXX On Windows, do \r\n?
sb.append("\n"); // NOI18N
sb.append(IndentUtils.createIndentString(doc, indent));
int insertOffset = offset; // offset < length ? offset+1 : offset;
doc.insertString(insertOffset, sb.toString(), null);
caret.setDot(insertOffset);
}
if (id == RubyTokenId.WHITESPACE) {
// Pressing newline in the whitespace before a comment
// should be identical to pressing newline with the caret
// at the beginning of the comment
int begin = Utilities.getRowFirstNonWhite(doc, offset);
if (begin != -1 && offset < begin) {
ts.move(begin);
if (ts.moveNext()) {
id = ts.token().id();
if (id == RubyTokenId.LINE_COMMENT) {
offset = begin;
}
}
}
}
if (id == RubyTokenId.LINE_COMMENT) {
// Only do this if the line only contains comments OR if there is content to the right on this line,
// or if the next line is a comment!
boolean continueComment = false;
int begin = Utilities.getRowFirstNonWhite(doc, offset);
// We should only continue comments if the previous line had a comment
// (and a comment from the beginning, not a trailing comment)
boolean previousLineWasComment = false;
int rowStart = Utilities.getRowStart(doc, offset);
if (rowStart > 0) {
int prevBegin = Utilities.getRowFirstNonWhite(doc, rowStart-1);
if (prevBegin != -1) {
Token<? extends RubyTokenId> firstToken = LexUtilities.getToken(doc, prevBegin);
if (firstToken != null && firstToken.id() == RubyTokenId.LINE_COMMENT) {
previousLineWasComment = true;
}
}
}
// See if we have more input on this comment line (to the right
// of the inserted newline); if so it's a "split" operation on
// the comment
if (previousLineWasComment || offset > begin) {
if (ts.offset()+token.length() > offset+1) {
// See if the remaining text is just whitespace
String trailing = doc.getText(offset,Utilities.getRowEnd(doc, offset)-offset);
if (trailing.trim().length() != 0) {
continueComment = true;
}
} else if (CONTINUE_COMMENTS) {
// See if the "continue comments" options is turned on, and this is a line that
// contains only a comment (after leading whitespace)
Token<? extends RubyTokenId> firstToken = LexUtilities.getToken(doc, begin);
if (firstToken != null && firstToken.id() == RubyTokenId.LINE_COMMENT) {
continueComment = true;
}
}
if (!continueComment) {
// See if the next line is a comment; if so we want to continue
// comments editing the middle of the comment
int nextLine = Utilities.getRowEnd(doc, offset)+1;
if (nextLine < doc.getLength()) {
int nextLineFirst = Utilities.getRowFirstNonWhite(doc, nextLine);
if (nextLineFirst != -1) {
Token<? extends RubyTokenId> firstToken = LexUtilities.getToken(doc, nextLineFirst);
if (firstToken != null && firstToken.id() == RubyTokenId.LINE_COMMENT) {
continueComment = true;
}
}
}
}
}
if (continueComment) {
// Line comments should continue
int indent = GsfUtilities.getLineIndent(doc, offset);
StringBuilder sb = new StringBuilder();
sb.append(IndentUtils.createIndentString(doc, indent));
sb.append("#"); // NOI18N
// Copy existing indentation
int afterHash = begin+1;
String line = doc.getText(afterHash, Utilities.getRowEnd(doc, afterHash)-afterHash);
for (int i = 0; i < line.length(); i++) {
char c = line.charAt(i);
if (c == ' ' || c == '\t') {
sb.append(c);
} else {
break;
}
}
int insertOffset = offset; // offset < length ? offset+1 : offset;
if (offset == begin && insertOffset > 0) {
insertOffset = Utilities.getRowStart(doc, offset);
int sp = Utilities.getRowStart(doc, offset)+sb.length();
doc.insertString(insertOffset, sb.toString(), null);
caret.setDot(sp);
return sp;
}
doc.insertString(insertOffset, sb.toString(), null);
caret.setDot(insertOffset);
return insertOffset+sb.length()+1;
}
}
return -1;
}
/**
* Determine if an "end" or "}" is missing following the caret offset.
* The logic used is to check the text on the current line for block initiators
* (e.g. "def", "for", "{" etc.) and then see if a corresponding close is
* found after the same indentation level.
*
* @param doc The document to be checked
* @param offset The offset of the current line
* @param skipJunk If false, only consider the current line (of the offset)
* as the possible "block opener"; if true, look backwards across empty
* lines and comment lines as well.
* @param insertEndResult Null, or a boolean 1-element array whose first
* element will be set to true iff this method determines that "end" should
* be inserted
* @param insertRBraceResult Null, or a boolean 1-element array whose first
* element will be set to true iff this method determines that "}" should
* be inserted
* @param startOffsetResult Null, or an integer 1-element array whose first
* element will be set to the starting offset of the opening block.
* @param indentResult Null, or an integer 1-element array whose first
* element will be set to the indentation level "end" or "}" should be
* indented to when inserted.
* @return true if something is missing; insertEndResult, insertRBraceResult
* and identResult will provide the more specific return values in their
* first elements.
*/
static boolean isEndMissing(BaseDocument doc, int offset, boolean skipJunk,
boolean[] insertEndResult, boolean[] insertRBraceResult, int[] startOffsetResult,
int[] indentResult) throws BadLocationException {
int length = doc.getLength();
// Insert an end statement? Insert a } marker?
// Do so if the current line contains an unmatched begin marker,
// AND a "corresponding" marker does not exist.
// This will be determined as follows: Look forward, and check
// that we don't have "indented" code already (tokens at an
// indentation level higher than the current line was), OR that
// there is no actual end or } coming up.
if (startOffsetResult != null) {
startOffsetResult[0] = Utilities.getRowFirstNonWhite(doc, offset);
}
int beginEndBalance = LexUtilities.getBeginEndLineBalance(doc, offset, true);
int braceBalance =
LexUtilities.getLineBalance(doc, offset, RubyTokenId.LBRACE, RubyTokenId.RBRACE);
if ((beginEndBalance == 1) || (braceBalance == 1)) {
// There is one more opening token on the line than a corresponding
// closing token. (If there's is more than one we don't try to help.)
int indent = GsfUtilities.getLineIndent(doc, offset);
// Look for the next nonempty line, and if its indent is > indent,
// or if its line balance is -1 (e.g. it's an end) we're done
boolean insertEnd = beginEndBalance > 0;
boolean insertRBrace = braceBalance > 0;
int next = Utilities.getRowEnd(doc, offset) + 1;
for (; next < length; next = Utilities.getRowEnd(doc, next) + 1) {
if (Utilities.isRowEmpty(doc, next) || Utilities.isRowWhite(doc, next) ||
LexUtilities.isCommentOnlyLine(doc, next)) {
continue;
}
int nextIndent = GsfUtilities.getLineIndent(doc, next);
if (nextIndent > indent) {
insertEnd = false;
insertRBrace = false;
} else if (nextIndent == indent) {
if (insertEnd) {
if (LexUtilities.getBeginEndLineBalance(doc, next, false) < 0) {
insertEnd = false;
} else {
// See if I have a structure word like "else", "ensure", etc.
// (These are indent words that are not also begin words)
// and if so refrain from inserting the end
int lineBegin = Utilities.getRowFirstNonWhite(doc, next);
Token<?extends RubyTokenId> token =
LexUtilities.getToken(doc, lineBegin);
if ((token != null) && LexUtilities.isIndentToken(token.id()) &&
!LexUtilities.isBeginToken(token.id(), doc, lineBegin)) {
insertEnd = false;
}
}
} else if (insertRBrace &&
(LexUtilities.getLineBalance(doc, next, RubyTokenId.LBRACE,
RubyTokenId.RBRACE) < 0)) {
insertRBrace = false;
}
}
break;
}
if (insertEndResult != null) {
insertEndResult[0] = insertEnd;
}
if (insertRBraceResult != null) {
insertRBraceResult[0] = insertRBrace;
}
if (indentResult != null) {
indentResult[0] = indent;
}
return insertEnd || insertRBrace;
}
return false;
}
public boolean beforeCharInserted(Document document, int caretOffset, JTextComponent target, char ch)
throws BadLocationException {
isAfter = false;
Caret caret = target.getCaret();
BaseDocument doc = (BaseDocument)document;
if (!isInsertMatchingEnabled(doc)) {
return false;
}
//dumpTokens(doc, caretOffset);
// Gotta look for the string begin pair in tokens since ANY character can
// be used in Ruby string like the %x!! form.
if (caretOffset == 0) {
return false;
}
if (target.getSelectionStart() != -1) {
if (GsfUtilities.isCodeTemplateEditing(doc)) {
int start = target.getSelectionStart();
int end = target.getSelectionEnd();
if (start < end) {
target.setSelectionStart(start);
target.setSelectionEnd(start);
caretOffset = start;
caret.setDot(caretOffset);
doc.remove(start, end-start);
}
// Fall through to do normal insert matching work
} else if (ch == '"' || ch == '\'' || ch == '(' || ch == '{' || ch == '[' || ch == '/') {
// Bracket the selection
String selection = target.getSelectedText();
if (selection != null && selection.length() > 0) {
char firstChar = selection.charAt(0);
if (firstChar != ch) {
int start = target.getSelectionStart();
int end = target.getSelectionEnd();
TokenSequence<? extends RubyTokenId> ts = LexUtilities.getPositionedSequence(doc, start);
if (ts != null && ts.token().id() != RubyTokenId.STRING_LITERAL &&
ts.token().id() != RubyTokenId.QUOTED_STRING_LITERAL &&
ts.token().id() != RubyTokenId.REGEXP_LITERAL) {
int lastChar = selection.charAt(selection.length()-1);
// Replace the surround-with chars?
if (selection.length() > 1 &&
((firstChar == '"' || firstChar == '\'' || firstChar == '(' ||
firstChar == '{' || firstChar == '[' || firstChar == '/') &&
lastChar == matching(firstChar))) {
doc.remove(end-1, 1);
doc.insertString(end-1, Character.toString(matching(ch)), null);
doc.remove(start, 1);
doc.insertString(start, Character.toString(ch), null);
target.getCaret().setDot(end);
} else {
// No, insert around
doc.remove(start,end-start);
doc.insertString(start, ch + selection + matching(ch), null);
target.getCaret().setDot(start+selection.length()+2);
}
return true;
}
}
}
} else if (ch == '#' &&
((LexUtilities.isInsideQuotedString(doc, target.getSelectionStart()) &&
LexUtilities.isInsideQuotedString(doc, target.getSelectionEnd())) ||
(LexUtilities.isInsideRegexp(doc, target.getSelectionStart()) &&
LexUtilities.isInsideRegexp(doc, target.getSelectionEnd())))) {
String selection = target.getSelectedText();
if (selection != null && selection.length() > 0 && selection.charAt(0) != ch) {
int start = target.getSelectionStart();
doc.remove(start, target.getSelectionEnd()-start);
doc.insertString(start, "#{" + selection + "}", null);
target.getCaret().setDot(start+selection.length()+3);
return true;
}
}
}
TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, caretOffset);
if (ts == null) {
return false;
}
ts.move(caretOffset);
if (!ts.moveNext() && !ts.movePrevious()) {
return false;
}
Token<?extends RubyTokenId> token = ts.token();
TokenId id = token.id();
TokenId[] stringTokens = null;
TokenId beginTokenId = null;
if (id == RubyTokenId.LINE_COMMENT && target.getSelectionStart() != -1) {
if (ch == '*' || ch == '+' || ch == '_') {
// See if it's a comment and if so surround the text with an rdoc modifier
// such as bold, teletype or italics
String selection = target.getSelectedText();
// Don't allow any spaces - you can't bracket multiple words in rdoc I think (TODO - check that)
if (selection != null && selection.length() > 0 && selection.charAt(0) != ch && selection.indexOf(' ') == -1) {
int start = target.getSelectionStart();
doc.remove(start, target.getSelectionEnd()-start);
doc.insertString(start, ch + selection + matching(ch), null);
target.getCaret().setDot(start+selection.length()+2);
return true;
}
}
}
// "/" is handled AFTER the character has been inserted since we need the lexer's help
if (ch == '\"') {
stringTokens = STRING_TOKENS;
beginTokenId = RubyTokenId.QUOTED_STRING_BEGIN;
} else if (ch == '\'') {
stringTokens = STRING_TOKENS;
beginTokenId = RubyTokenId.STRING_BEGIN;
} else if (id == RubyTokenId.ERROR) {
String text = token.text().toString();
if (text.equals("%")) {
// Depending on the character we're going to continue
if (!Character.isLetter(ch)) { // %q, %x, etc. Only %[], %!!, %<space> etc. is allowed
stringTokens = STRING_TOKENS;
beginTokenId = RubyTokenId.QUOTED_STRING_BEGIN;
}
} else if ((text.length() == 2) && (text.charAt(0) == '%') &&
Character.isLetter(text.charAt(1))) {
char c = text.charAt(1);
switch (c) {
case 'q':
stringTokens = STRING_TOKENS;
beginTokenId = RubyTokenId.STRING_BEGIN;
break;
case 'Q':
stringTokens = STRING_TOKENS;
beginTokenId = RubyTokenId.QUOTED_STRING_BEGIN;
break;
case 'r':
stringTokens = REGEXP_TOKENS;
beginTokenId = RubyTokenId.REGEXP_BEGIN;
break;
default:
// ?
stringTokens = STRING_TOKENS;
beginTokenId = RubyTokenId.QUOTED_STRING_BEGIN;
}
} else {
ts.movePrevious();
TokenId prevId = ts.token().id();
if ((prevId == RubyTokenId.STRING_BEGIN) ||
(prevId == RubyTokenId.QUOTED_STRING_BEGIN)) {
stringTokens = STRING_TOKENS;
beginTokenId = prevId;
} else if (prevId == RubyTokenId.REGEXP_BEGIN) {
stringTokens = REGEXP_TOKENS;
beginTokenId = RubyTokenId.REGEXP_BEGIN;
}
}
} else if (((((id == RubyTokenId.STRING_BEGIN) || (id == RubyTokenId.QUOTED_STRING_BEGIN)) &&
(caretOffset == (ts.offset() + 1))))) {
if (!Character.isLetter(ch)) { // %q, %x, etc. Only %[], %!!, %<space> etc. is allowed
stringTokens = STRING_TOKENS;
beginTokenId = id;
}
} else if (((id == RubyTokenId.STRING_BEGIN) && (caretOffset == (ts.offset() + 2))) ||
(id == RubyTokenId.STRING_END)) {
stringTokens = STRING_TOKENS;
beginTokenId = RubyTokenId.STRING_BEGIN;
} else if (((id == RubyTokenId.QUOTED_STRING_BEGIN) && (caretOffset == (ts.offset() + 2))) ||
(id == RubyTokenId.QUOTED_STRING_END)) {
stringTokens = STRING_TOKENS;
beginTokenId = RubyTokenId.QUOTED_STRING_BEGIN;
} else if (((id == RubyTokenId.REGEXP_BEGIN) && (caretOffset == (ts.offset() + 2))) ||
(id == RubyTokenId.REGEXP_END)) {
stringTokens = REGEXP_TOKENS;
beginTokenId = RubyTokenId.REGEXP_BEGIN;
}
if (stringTokens != null) {
boolean inserted =
completeQuote(doc, caretOffset, caret, ch, stringTokens, beginTokenId);
if (inserted) {
caret.setDot(caretOffset + 1);
return true;
} else {
return false;
}
}
return false;
}
// For debugging purposes
// Probably obsolete - see the tokenspy utility in gsf debugging tools for better help
//private void dumpTokens(BaseDocument doc, int dot) {
// TokenSequence< ?extends RubyTokenId> ts = LexUtilities.getTokenSequence(doc);
//
// System.out.println("Dumping tokens for dot=" + dot);
// int prevOffset = -1;
// if (ts != null) {
// ts.moveFirst();
// int index = 0;
// do {
// Token<? extends RubyTokenId> token = ts.token();
// int offset = ts.offset();
// String id = token.id().toString();
// String text = token.text().toString().replaceAll("\n", "\\\\n");
// if (prevOffset < dot && dot <= offset) {
// System.out.print(" ===> ");
// }
// System.out.println("Token " + index + ": offset=" + offset + ": id=" + id + ": text=" + text);
// index++;
// prevOffset = offset;
// } while (ts.moveNext());
// }
//}
/**
* A hook method called after a character was inserted into the
* document. The function checks for special characters for
* completion ()[]'"{} and other conditions and optionally performs
* changes to the doc and or caret (complets braces, moves caret,
* etc.)
* @param document the document where the change occurred
* @param dotPos position of the character insertion
* @param target The target
* @param ch the character that was inserted
* @return Whether the insert was handled
* @throws BadLocationException if dotPos is not correct
*/
public boolean afterCharInserted(Document document, int dotPos, JTextComponent target, char ch)
throws BadLocationException {
isAfter = true;
Caret caret = target.getCaret();
BaseDocument doc = (BaseDocument)document;
if (REFLOW_COMMENTS) {
Token<?extends RubyTokenId> token = LexUtilities.getToken(doc, dotPos);
if (token != null) {
TokenId id = token.id();
if (id == RubyTokenId.LINE_COMMENT || id == RubyTokenId.DOCUMENTATION) {
ReflowParagraphAction.reflowEditedComment(target);
}
}
}
// See if our automatic adjustment of indentation when typing (for example) "end" was
// premature - if you were typing a longer word beginning with one of my adjustment
// prefixes, such as "endian", then put the indentation back.
if (previousAdjustmentOffset != -1) {
if (dotPos == previousAdjustmentOffset) {
// Revert indentation iff the character at the insert position does
// not start a new token (e.g. the previous token that we reindented
// was not complete)
TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, dotPos);
if (ts != null) {
ts.move(dotPos);
if (ts.moveNext() && (ts.offset() < dotPos)) {
GsfUtilities.setLineIndentation(doc, dotPos, previousAdjustmentIndent);
}
}
}
previousAdjustmentOffset = -1;
}
//dumpTokens(doc, dotPos);
switch (ch) {
case '#': {
// Automatically insert #{^} when typing "#" in a quoted string or regexp
Token<?extends RubyTokenId> token = LexUtilities.getToken(doc, dotPos);
if (token == null) {
return true;
}
TokenId id = token.id();
if (id == RubyTokenId.QUOTED_STRING_LITERAL || id == RubyTokenId.REGEXP_LITERAL) {
document.insertString(dotPos+1, "{}", null);
// Skip the "{" to place the caret between { and }
caret.setDot(dotPos+2);
}
break;
}
case '}':
case '{':
case ')':
case ']':
case '(':
case '[': {
if (!isInsertMatchingEnabled(doc)) {
return false;
}
Token<?extends RubyTokenId> token = LexUtilities.getToken(doc, dotPos);
if (token == null) {
return true;
}
TokenId id = token.id();
if ((ch == '{') && (id == RubyTokenId.ERROR && dotPos > 0)) {
Token<? extends RubyTokenId> prevToken = LexUtilities.getToken(doc, dotPos-1);
if (prevToken != null) {
TokenId prevId = prevToken.id();
if (prevId == RubyTokenId.STRING_LITERAL || prevId == RubyTokenId.REGEXP_LITERAL) {
// Avoid case where typing "#{" ends up as #{{ if user
// isn't used to the #{^} auto-insertion
if (dotPos > 1) {
String s = doc.getText(dotPos-2, 2);
if ("#{".equals(s)) { // NOI18N
doc.remove(dotPos, 1);
caret.setDot(dotPos); // skip closing bracket
return true;
}
}
}
}
}
if (ch == '}' && (id == RubyTokenId.QUOTED_STRING_LITERAL || id == RubyTokenId.REGEXP_LITERAL)) {
Token<? extends RubyTokenId> prevToken = LexUtilities.getToken(doc, dotPos-1);
if (prevToken != null) {
TokenId prevId = prevToken.id();
if (prevId == RubyTokenId.EMBEDDED_RUBY) {
if (dotPos < doc.getLength()-1) {
char c = doc.getText(dotPos+1, 1).charAt(0);
if (c == '}') {
doc.remove(dotPos, 1);
caret.setDot(dotPos+1); // skip closing bracket
return true;
}
}
}
}
}
// This only kicks in when #{} has no content
if ((ch == '}') && id == RubyTokenId.EMBEDDED_RUBY) {
// Support type-through of } when we have #{^}
if (dotPos < doc.getLength()-1) {
char c = doc.getText(dotPos+1, 1).charAt(0);
if (c == '}') {
doc.remove(dotPos, 1);
caret.setDot(dotPos+1); // skip closing bracket
return true;
}
}
}
if (id == RubyTokenId.ANY_OPERATOR) {
int length = token.length();
String s = token.text().toString();
if ((length == 2) && "[]".equals(s) || "[]=".equals(s)) { // Special case
skipClosingBracket(doc, caret, ch, RubyTokenId.RBRACKET);
return true;
}
}
if (((id == RubyTokenId.IDENTIFIER) && (token.length() == 1)) ||
(id == RubyTokenId.LBRACKET) || (id == RubyTokenId.RBRACKET) ||
(id == RubyTokenId.LBRACE) || (id == RubyTokenId.RBRACE) ||
(id == RubyTokenId.LPAREN) || (id == RubyTokenId.RPAREN)) {
if (ch == ']') {
skipClosingBracket(doc, caret, ch, RubyTokenId.RBRACKET);
} else if (ch == ')') {
skipClosingBracket(doc, caret, ch, RubyTokenId.RPAREN);
} else if (ch == '}') {
skipClosingBracket(doc, caret, ch, RubyTokenId.RBRACE);
} else if ((ch == '[') || (ch == '(') || (ch == '{')) {
completeOpeningBracket(doc, dotPos, caret, ch);
}
}
// Reindent blocks (won't do anything if } is not at the beginning of a line
if (ch == '}') {
reindent(doc, dotPos, RubyTokenId.RBRACE, caret);
} else if (ch == ']') {
reindent(doc, dotPos, RubyTokenId.RBRACKET, caret);
}
}
break;
case 'd':
// See if it's the end of an "end" - if so, reindent
reindent(doc, dotPos, RubyTokenId.END, caret);
break;
case 'e':
// See if it's the end of an "else" or an "ensure" - if so, reindent
reindent(doc, dotPos, RubyTokenId.ELSE, caret);
reindent(doc, dotPos, RubyTokenId.ENSURE, caret);
reindent(doc, dotPos, RubyTokenId.RESCUE, caret);
break;
case 'f':
// See if it's the end of an "else" - if so, reindent
reindent(doc, dotPos, RubyTokenId.ELSIF, caret);
break;
case 'n':
// See if it's the end of an "when" - if so, reindent
reindent(doc, dotPos, RubyTokenId.WHEN, caret);
break;
case '/': {
if (!isInsertMatchingEnabled(doc)) {
return false;
}
// Bracket matching for regular expressions has to be done AFTER the
// character is inserted into the document such that I can use the lexer
// to determine whether it's a division (e.g. x/y) or a regular expression (/foo/)
Token<?extends RubyTokenId> token = LexUtilities.getToken(doc, dotPos);
if (token != null) {
TokenId id = token.id();
if (id == RubyTokenId.REGEXP_BEGIN || id == RubyTokenId.REGEXP_END) {
TokenId[] stringTokens = REGEXP_TOKENS;
TokenId beginTokenId = RubyTokenId.REGEXP_BEGIN;
boolean inserted =
completeQuote(doc, dotPos, caret, ch, stringTokens, beginTokenId);
if (inserted) {
caret.setDot(dotPos + 1);
}
return inserted;
}
}
break;
}
case '|': {
if (!isInsertMatchingEnabled(doc)) {
return false;
}
Token<?extends RubyTokenId> token = LexUtilities.getToken(doc, dotPos);
if (token == null) {
return true;
}
TokenId id = token.id();
// Ensure that we're not in a comment, strings etc.
if (id == RubyTokenId.NONUNARY_OP && token.length() == 2 && "||".equals(token.text().toString())) {
// Type through: |^| and type |
// See if we're in a do or { block
if (isBlockDefinition(doc, dotPos)) {
// It's a block so this should be a variable declaration of the form { |foo| }
// Did you type "|" in the middle? If so, should type through!
// TODO - check that we were typing in the middle, not in the front!
doc.remove(dotPos, 1);
caret.setDot(dotPos+1);
return true;
}
} else if (id == RubyTokenId.IDENTIFIER && token.length() == 1 && "|".equals(token.text().toString())) {
// Only insert a matching | if there aren't any others on this line AND we're in a block
if (isBlockDefinition(doc, dotPos)) {
boolean found = false;
int lineEnd = Utilities.getRowEnd(doc, dotPos);
if (lineEnd > dotPos+1) {
TokenSequence<? extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, dotPos+1);
ts.move(dotPos+1);
while (ts.moveNext() && ts.offset() < lineEnd) {
Token<? extends RubyTokenId> t = ts.token();
if (t.id() == RubyTokenId.IDENTIFIER && t.length() == 1 && "|".equals(t.text().toString())) {
found = true;
break;
}
}
}
if (!found) {
doc.insertString(dotPos+1, "|", null);
caret.setDot(dotPos + 1);
}
}
return true;
}
break;
}
}
return true;
}
private boolean isBlockDefinition(BaseDocument doc, int dotPos) throws BadLocationException {
TokenSequence<? extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, dotPos);
int lineStart = Utilities.getRowStart(doc, dotPos);
ts.move(dotPos+1);
while (ts.movePrevious() && ts.offset() >= lineStart) {
TokenId tid = ts.token().id();
if (tid == RubyTokenId.DO || tid == RubyTokenId.LBRACE) {
return true;
// } else if (tid == RubyTokenId.IDENTIFIER && ts.token().length() == 1 && "|".equals(ts.token().text().toString())) {
// continue;
// } else if (tid == RubyTokenId.NONUNARY_OP && "||".equals(ts.token().text().toString())) {
// continue;
} else if (tid == RubyTokenId.END || tid == RubyTokenId.RBRACE) {
break;
}
}
return false;
}
private void reindent(BaseDocument doc, int offset, TokenId id, Caret caret)
throws BadLocationException {
TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, offset);
if (ts != null) {
ts.move(offset);
if (!ts.moveNext() && !ts.movePrevious()) {
return;
}
Token<?extends RubyTokenId> token = ts.token();
if ((token.id() == id)) {
final int rowFirstNonWhite = Utilities.getRowFirstNonWhite(doc, offset);
// Ensure that this token is at the beginning of the line
if (ts.offset() > rowFirstNonWhite) {
if (RubyUtils.isRhtmlDocument(doc)) {
// Allow "<%[whitespace]*" to preceed
String s = doc.getText(rowFirstNonWhite, ts.offset()-rowFirstNonWhite);
if (!s.matches("<%\\s*")) {
return;
}
} else {
return;
}
}
OffsetRange begin;
if (id == RubyTokenId.RBRACE) {
begin = LexUtilities.findBwd(doc, ts, RubyTokenId.LBRACE, RubyTokenId.RBRACE);
} else if (id == RubyTokenId.RBRACKET) {
begin = LexUtilities.findBwd(doc, ts, RubyTokenId.LBRACKET, RubyTokenId.RBRACKET);
} else {
begin = LexUtilities.findBegin(doc, ts);
}
if (begin != OffsetRange.NONE) {
int beginOffset = begin.getStart();
int indent = GsfUtilities.getLineIndent(doc, beginOffset);
previousAdjustmentIndent = GsfUtilities.getLineIndent(doc, offset);
GsfUtilities.setLineIndentation(doc, offset, indent);
previousAdjustmentOffset = caret.getDot();
}
}
}
}
/** Replaced by RubyBracesMatcher */
public OffsetRange findMatching(Document document, int offset /*, boolean simpleSearch*/) {
return OffsetRange.NONE;
}
/**
* Hook called after a character *ch* was backspace-deleted from
* *doc*. The function possibly removes bracket or quote pair if
* appropriate.
* @param doc the document
* @param dotPos position of the change
* @param caret caret
* @param ch the character that was deleted
*/
@SuppressWarnings("fallthrough")
public boolean charBackspaced(Document document, int dotPos, JTextComponent target, char ch)
throws BadLocationException {
BaseDocument doc = (BaseDocument)document;
switch (ch) {
case ' ': {
// Backspacing over "# " ? Delete the "#" too!
TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, dotPos);
ts.move(dotPos);
if ((ts.moveNext() || ts.movePrevious()) && (ts.offset() == dotPos-1 && ts.token().id() == RubyTokenId.LINE_COMMENT)) {
doc.remove(dotPos-1, 1);
target.getCaret().setDot(dotPos-1);
return true;
}
break;
}
case '{': {
// Attempt to fix #{} in chars
Token<? extends RubyTokenId> token = LexUtilities.getToken(doc, dotPos-1);
if (token != null && (token.id() == RubyTokenId.QUOTED_STRING_LITERAL || token.id() == RubyTokenId.REGEXP_LITERAL)) {
String s = document.getText(dotPos-1, 2);
if ("#}".equals(s)) {
// We have just deleted a #{} segment
doc.remove(dotPos, 1);
target.getCaret().setDot(dotPos);
}
}
// Fall through to handle '{' typethrough as well
}
case '(':
case '[': { // and '{' via fallthrough
char tokenAtDot = LexUtilities.getTokenChar(doc, dotPos);
if (((tokenAtDot == ']') &&
(LexUtilities.getTokenBalance(doc, RubyTokenId.LBRACKET, RubyTokenId.RBRACKET, dotPos) != 0)) ||
((tokenAtDot == ')') &&
(LexUtilities.getTokenBalance(doc, RubyTokenId.LPAREN, RubyTokenId.RPAREN, dotPos) != 0)) ||
((tokenAtDot == '}') &&
(LexUtilities.getTokenBalance(doc, RubyTokenId.LBRACE, RubyTokenId.RBRACE, dotPos) != 0))) {
doc.remove(dotPos, 1);
}
break;
}
case '|':
case '\"':
case '\'':
case '/': {
char[] match = doc.getChars(dotPos, 1);
if ((match != null) && (match[0] == ch)) {
doc.remove(dotPos, 1);
}
} // TODO: Test other auto-completion chars, like %q-foo-
}
return true;
}
/**
* A hook to be called after closing bracket ) or ] was inserted into
* the document. The method checks if the bracket should stay there
* or be removed and some exisitng bracket just skipped.
*
* @param doc the document
* @param dotPos position of the inserted bracket
* @param caret caret
* @param bracket the bracket character ']' or ')'
*/
private void skipClosingBracket(BaseDocument doc, Caret caret, char bracket, TokenId bracketId)
throws BadLocationException {
int caretOffset = caret.getDot();
if (isSkipClosingBracket(doc, caretOffset, bracketId)) {
doc.remove(caretOffset - 1, 1);
caret.setDot(caretOffset); // skip closing bracket
}
}
/**
* Check whether the typed bracket should stay in the document
* or be removed.
* <br>
* This method is called by <code>skipClosingBracket()</code>.
*
* @param doc document into which typing was done.
* @param caretOffset
*/
private boolean isSkipClosingBracket(BaseDocument doc, int caretOffset, TokenId bracketId)
throws BadLocationException {
// First check whether the caret is not after the last char in the document
// because no bracket would follow then so it could not be skipped.
if (caretOffset == doc.getLength()) {
return false; // no skip in this case
}
boolean skipClosingBracket = false; // by default do not remove
TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, caretOffset);
if (ts == null) {
return false;
}
// XXX BEGIN TOR MODIFICATIONS
//ts.move(caretOffset+1);
ts.move(caretOffset);
if (!ts.moveNext()) {
return false;
}
Token<?extends RubyTokenId> token = ts.token();
// Check whether character follows the bracket is the same bracket
if ((token != null) && (token.id() == bracketId)) {
int bracketIntId = bracketId.ordinal();
int leftBracketIntId =
(bracketIntId == RubyTokenId.RPAREN.ordinal()) ? RubyTokenId.LPAREN.ordinal()
: RubyTokenId.LBRACKET.ordinal();
// Skip all the brackets of the same type that follow the last one
ts.moveNext();
Token<?extends RubyTokenId> nextToken = ts.token();
while ((nextToken != null) && (nextToken.id() == bracketId)) {
token = nextToken;
if (!ts.moveNext()) {
break;
}
nextToken = ts.token();
}
// token var points to the last bracket in a group of two or more right brackets
// Attempt to find the left matching bracket for it
// Search would stop on an extra opening left brace if found
int braceBalance = 0; // balance of '{' and '}'
int bracketBalance = -1; // balance of the brackets or parenthesis
Token<?extends RubyTokenId> lastRBracket = token;
ts.movePrevious();
token = ts.token();
boolean finished = false;
while (!finished && (token != null)) {
int tokenIntId = token.id().ordinal();
if ((token.id() == RubyTokenId.LPAREN) || (token.id() == RubyTokenId.LBRACKET)) {
if (tokenIntId == bracketIntId) {
bracketBalance++;
if (bracketBalance == 0) {
if (braceBalance != 0) {
// Here the bracket is matched but it is located
// inside an unclosed brace block
// e.g. ... ->( } a()|)
// which is in fact illegal but it's a question
// of what's best to do in this case.
// We chose to leave the typed bracket
// by setting bracketBalance to 1.
// It can be revised in the future.
bracketBalance = 1;
}
finished = true;
}
}
} else if ((token.id() == RubyTokenId.RPAREN) ||
(token.id() == RubyTokenId.RBRACKET)) {
if (tokenIntId == bracketIntId) {
bracketBalance--;
}
} else if (token.id() == RubyTokenId.LBRACE) {
braceBalance++;
if (braceBalance > 0) { // stop on extra left brace
finished = true;
}
} else if (token.id() == RubyTokenId.RBRACE) {
braceBalance--;
}
if (!ts.movePrevious()) {
break;
}
token = ts.token();
}
if (bracketBalance != 0) { // not found matching bracket
// Remove the typed bracket as it's unmatched
skipClosingBracket = true;
} else { // the bracket is matched
// Now check whether the bracket would be matched
// when the closing bracket would be removed
// i.e. starting from the original lastRBracket token
// and search for the same bracket to the right in the text
// The search would stop on an extra right brace if found
braceBalance = 0;
bracketBalance = 1; // simulate one extra left bracket
//token = lastRBracket.getNext();
TokenHierarchy<BaseDocument> th = TokenHierarchy.get(doc);
int ofs = lastRBracket.offset(th);
ts.move(ofs);
ts.moveNext();
token = ts.token();
finished = false;
while (!finished && (token != null)) {
//int tokenIntId = token.getTokenID().getNumericID();
if ((token.id() == RubyTokenId.LPAREN) || (token.id() == RubyTokenId.LBRACKET)) {
if (token.id().ordinal() == leftBracketIntId) {
bracketBalance++;
}
} else if ((token.id() == RubyTokenId.RPAREN) ||
(token.id() == RubyTokenId.RBRACKET)) {
if (token.id().ordinal() == bracketIntId) {
bracketBalance--;
if (bracketBalance == 0) {
if (braceBalance != 0) {
// Here the bracket is matched but it is located
// inside an unclosed brace block
// which is in fact illegal but it's a question
// of what's best to do in this case.
// We chose to leave the typed bracket
// by setting bracketBalance to -1.
// It can be revised in the future.
bracketBalance = -1;
}
finished = true;
}
}
} else if (token.id() == RubyTokenId.LBRACE) {
braceBalance++;
} else if (token.id() == RubyTokenId.RBRACE) {
braceBalance--;
if (braceBalance < 0) { // stop on extra right brace
finished = true;
}
}
//token = token.getPrevious(); // done regardless of finished flag state
if (!ts.movePrevious()) {
break;
}
token = ts.token();
}
// If bracketBalance == 0 the bracket would be matched
// by the bracket that follows the last right bracket.
skipClosingBracket = (bracketBalance == 0);
}
}
return skipClosingBracket;
}
/**
* Check for various conditions and possibly add a pairing bracket
* to the already inserted.
* @param doc the document
* @param dotPos position of the opening bracket (already in the doc)
* @param caret caret
* @param bracket the bracket that was inserted
*/
private void completeOpeningBracket(BaseDocument doc, int dotPos, Caret caret, char bracket)
throws BadLocationException {
if (isCompletablePosition(doc, dotPos + 1)) {
String matchingBracket = "" + matching(bracket);
doc.insertString(dotPos + 1, matchingBracket, null);
caret.setDot(dotPos + 1);
}
}
// XXX TODO Use embedded string sequence here and see if it
// really is escaped. I know where those are!
// TODO Adjust for Ruby
private boolean isEscapeSequence(BaseDocument doc, int dotPos)
throws BadLocationException {
if (dotPos <= 0) {
return false;
}
char previousChar = doc.getChars(dotPos - 1, 1)[0];
return previousChar == '\\';
}
/**
* Check for conditions and possibly complete an already inserted
* quote .
* @param doc the document
* @param dotPos position of the opening bracket (already in the doc)
* @param caret caret
* @param bracket the character that was inserted
*/
private boolean completeQuote(BaseDocument doc, int dotPos, Caret caret, char bracket,
TokenId[] stringTokens, TokenId beginToken) throws BadLocationException {
if (isEscapeSequence(doc, dotPos)) { // \" or \' typed
return false;
}
// Examine token at the caret offset
if (doc.getLength() < dotPos) {
return false;
}
TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, dotPos);
if (ts == null) {
return false;
}
ts.move(dotPos);
if (!ts.moveNext() && !ts.movePrevious()) {
return false;
}
Token<?extends RubyTokenId> token = ts.token();
Token<?extends RubyTokenId> previousToken = null;
if (ts.movePrevious()) {
previousToken = ts.token();
}
int lastNonWhite = Utilities.getRowLastNonWhite(doc, dotPos);
// eol - true if the caret is at the end of line (ignoring whitespaces)
boolean eol = lastNonWhite < dotPos;
if ((token.id() == RubyTokenId.BLOCK_COMMENT) || (token.id() == RubyTokenId.LINE_COMMENT) ||
(token.id() == RubyTokenId.DOCUMENTATION)) { // 105419
return false;
} else if ((token.id() == RubyTokenId.WHITESPACE) && eol && ((dotPos - 1) > 0)) {
// check if the caret is at the very end of the line comment
token = LexUtilities.getToken(doc, dotPos - 1);
if (token != null && token.id() == RubyTokenId.LINE_COMMENT) {
return false;
}
}
boolean completablePosition = isQuoteCompletablePosition(doc, dotPos);
boolean insideString = false;
TokenId id = token.id();
for (TokenId currId : stringTokens) {
if (id == currId) {
insideString = true;
break;
}
}
if ((id == RubyTokenId.ERROR) && (previousToken != null) &&
(previousToken.id() == beginToken)) {
insideString = true;
}
if (!insideString) {
// check if the caret is at the very end of the line and there
// is an unterminated string literal
if ((token.id() == RubyTokenId.WHITESPACE) && eol) {
if ((dotPos - 1) > 0) {
token = LexUtilities.getToken(doc, dotPos - 1);
// XXX TODO use language embedding to handle this
insideString = (token != null && token.id() == RubyTokenId.STRING_LITERAL);
}
}
}
if (insideString) {
if (eol) {
return false; // do not complete
} else {
//#69524
char chr = doc.getChars(dotPos, 1)[0];
if (chr == bracket) {
if (!isAfter) {
doc.insertString(dotPos, "" + bracket, null); //NOI18N
} else {
if (!(dotPos < doc.getLength()-1 && doc.getText(dotPos+1,1).charAt(0) == bracket)) {
return true;
}
}
doc.remove(dotPos, 1);
return true;
}
}
}
if ((completablePosition && !insideString) || eol) {
doc.insertString(dotPos, "" + bracket + (isAfter ? "" : matching(bracket)), null); //NOI18N
return true;
}
return false;
}
/**
* Checks whether dotPos is a position at which bracket and quote
* completion is performed. Brackets and quotes are not completed
* everywhere but just at suitable places .
* @param doc the document
* @param dotPos position to be tested
*/
private boolean isCompletablePosition(BaseDocument doc, int dotPos)
throws BadLocationException {
if (dotPos == doc.getLength()) { // there's no other character to test
return true;
} else {
// test that we are in front of ) , " or '
char chr = doc.getChars(dotPos, 1)[0];
return ((chr == ')') || (chr == ',') || (chr == '\"') || (chr == '\'') || (chr == ' ') ||
(chr == ']') || (chr == '}') || (chr == '\n') || (chr == '\t') || (chr == ';'));
}
}
private boolean isQuoteCompletablePosition(BaseDocument doc, int dotPos)
throws BadLocationException {
if (dotPos == doc.getLength()) { // there's no other character to test
return true;
} else {
// test that we are in front of ) , " or ' ... etc.
int eol = Utilities.getRowEnd(doc, dotPos);
if ((dotPos == eol) || (eol == -1)) {
return false;
}
int firstNonWhiteFwd = Utilities.getFirstNonWhiteFwd(doc, dotPos, eol);
if (firstNonWhiteFwd == -1) {
return false;
}
char chr = doc.getChars(firstNonWhiteFwd, 1)[0];
if (chr == '%' && (RubyUtils.isRhtmlDocument(doc) || RubyUtils.isYamlDocument(doc)) ) {
return true;
}
return ((chr == ')') || (chr == ',') || (chr == '+') || (chr == '}') || (chr == ';') ||
(chr == ']') || (chr == '/'));
}
}
/**
* Returns for an opening bracket or quote the appropriate closing
* character.
*/
private char matching(char bracket) {
switch (bracket) {
case '(':
return ')';
case '/':
return '/';
case '[':
return ']';
case '\"':
return '\"'; // NOI18N
case '\'':
return '\'';
case '{':
return '}';
case '}':
return '{';
default:
return bracket;
}
}
public List<OffsetRange> findLogicalRanges(ParserResult info, int caretOffset) {
Node root = AstUtilities.getRoot(info);
if (root == null) {
return Collections.emptyList();
}
int astOffset = AstUtilities.getAstOffset(info, caretOffset);
if (astOffset == -1) {
return Collections.emptyList();
}
AstPath path = new AstPath(root, astOffset);
List<OffsetRange> ranges = new ArrayList<OffsetRange>();
/** Furthest we can go back in the buffer (in RHTML documents, this
* may be limited to the surrounding <% starting tag
*/
int min = 0;
int max = Integer.MAX_VALUE;
int length;
// 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 {
BaseDocument doc = RubyUtils.getDocument(info);
if (doc == null) {
return ranges;
}
length = doc.getLength();
if (RubyUtils.isRhtmlDocument(doc) || RubyUtils.isYamlDocument(doc)) {
TokenHierarchy th = TokenHierarchy.get(doc);
TokenSequence ts = th.tokenSequence();
ts.move(caretOffset);
if (ts.moveNext() || ts.movePrevious()) {
Token t = ts.token();
if (t.id().primaryCategory().startsWith("ruby")) { // NOI18N
min = ts.offset();
max = min+t.length();
// Try to extend with delimiters too
if (ts.movePrevious()) {
t = ts.token();
if ("ruby-delimiter".equals(t.id().primaryCategory())) { // NOI18N
min = ts.offset();
if (ts.moveNext() && ts.moveNext()) {
t = ts.token();
if ("ruby-delimiter".equals(t.id().primaryCategory())) { // NOI18N
max = ts.offset()+t.length();
}
}
}
}
}
}
}
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)) {
ranges.add(new OffsetRange(Utilities.getRowFirstNonWhite(doc, begin),
Utilities.getRowLastNonWhite(doc, end)+1));
int lineBegin = begin;
int lineEnd = end;
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;
}
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 ((lineBegin > begin) || (lineEnd < end)) {
ranges.add(new OffsetRange(begin, end));
}
} else {
// It's just a line comment next to some code; select the comment
TokenHierarchy<Document> th = TokenHierarchy.get((Document)doc);
int offset = token.offset(th);
ranges.add(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();
ranges.add(new OffsetRange(begin, end));
}
} catch (BadLocationException ble) {
return ranges;
}
Iterator<Node> it = path.leafToRoot();
OffsetRange previous = OffsetRange.NONE;
while (it.hasNext()) {
Node node = it.next();
// Filter out some uninteresting nodes
if (node.getNodeType() == NodeType.NEWLINENODE) {
continue;
}
OffsetRange range = AstUtilities.getRange(node);
if (node.getNodeType() == NodeType.CALLNODE && ranges.size() == 0 && node == path.leaf()) {
// Try to handle scenarios like issue 111941 - in a call like
// foo.bar.snark
// there's no AST node for the "bar" part - only a CallNode for "foo.bar",
// so add in an extra range for this case
Node receiver = ((CallNode)node).getReceiverNode();
OffsetRange receiverRange = AstUtilities.getRange(receiver);
if (receiver != null && astOffset > receiverRange.getEnd() && receiverRange.getEnd()+1 < range.getEnd()) {
ranges.add(new OffsetRange(receiverRange.getEnd()+1, range.getEnd()));
}
}
// The contains check should be unnecessary, but I end up getting
// some weird positions for some JRuby AST nodes
if (range.containsInclusive(astOffset) && !range.equals(previous)) {
range = LexUtilities.getLexerOffsets(info, range);
if (range != OffsetRange.NONE) {
if (range.getStart() < min) {
ranges.add(new OffsetRange(min, max));
ranges.add(new OffsetRange(0, length));
break;
}
ranges.add(range);
previous = range;
}
}
}
return ranges;
}
// UGH - this method has gotten really ugly after successive refinements based on unit tests - consider cleaning up
public int getNextWordOffset(Document document, int offset, boolean reverse) {
BaseDocument doc = (BaseDocument)document;
TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, offset);
if (ts == null) {
return -1;
}
ts.move(offset);
if (!ts.moveNext() && !ts.movePrevious()) {
return -1;
}
if (reverse && ts.offset() == offset) {
if (!ts.movePrevious()) {
return -1;
}
}
Token<? extends RubyTokenId> token = ts.token();
TokenId id = token.id();
if (id == RubyTokenId.WHITESPACE) {
// Just eat up the space in the normal IDE way
if ((reverse && ts.offset() < offset) || (!reverse && ts.offset() > offset)) {
return ts.offset();
}
while (id == RubyTokenId.WHITESPACE) {
if (reverse && !ts.movePrevious()) {
return -1;
} else if (!reverse && !ts.moveNext()) {
return -1;
}
token = ts.token();
id = token.id();
}
if (reverse) {
int start = ts.offset()+token.length();
if (start < offset) {
return start;
}
} else {
int start = ts.offset();
if (start > offset) {
return start;
}
}
}
if (id == RubyTokenId.IDENTIFIER || id == RubyTokenId.TYPE_SYMBOL ||
id == RubyTokenId.CONSTANT ||
id == RubyTokenId.GLOBAL_VAR || id == RubyTokenId.INSTANCE_VAR) {
String s = token.text().toString();
int length = s.length();
int wordOffset = offset-ts.offset();
if (reverse) {
// Find previous
int offsetInImage = offset - 1 - ts.offset();
if (offsetInImage < 0) {
return -1;
}
if (offsetInImage < length && Character.isUpperCase(s.charAt(offsetInImage))) {
for (int i = offsetInImage - 1; i >= 0; i--) {
char charAtI = s.charAt(i);
if (charAtI == '_') {
// return offset of previous uppercase char in the identifier
return ts.offset() + i + 1;
} else if (!Character.isUpperCase(charAtI)) {
// return offset of previous uppercase char in the identifier
return ts.offset() + i + 1;
}
}
return ts.offset();
} else {
for (int i = offsetInImage - 1; i >= 0; i--) {
char charAtI = s.charAt(i);
if (charAtI == '_') {
return ts.offset() + i + 1;
}
if (Character.isUpperCase(charAtI)) {
// now skip over previous uppercase chars in the identifier
for (int j = i; j >= 0; j--) {
char charAtJ = s.charAt(j);
if (charAtJ == '_') {
return ts.offset() + j+1;
}
if (!Character.isUpperCase(charAtJ)) {
// return offset of previous uppercase char in the identifier
return ts.offset() + j + 1;
}
}
return ts.offset();
}
}
return ts.offset();
}
} else {
// Find next
int start = wordOffset+1;
if (wordOffset < 0 || wordOffset >= s.length()) {
// Probably the end of a token sequence, such as this:
// <%s|%>
return -1;
}
if (Character.isUpperCase(s.charAt(wordOffset))) {
// if starting from a Uppercase char, first skip over follwing upper case chars
for (int i = start; i < length; i++) {
char charAtI = s.charAt(i);
if (!Character.isUpperCase(charAtI)) {
break;
}
if (s.charAt(i) == '_') {
return ts.offset()+i;
}
start++;
}
}
for (int i = start; i < length; i++) {
char charAtI = s.charAt(i);
if (charAtI == '_' || Character.isUpperCase(charAtI)) {
return ts.offset()+i;
}
}
}
}
// Default handling in the IDE
return -1;
}
}