package com.redhat.ceylon.eclipse.code.editor;
import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.ASTRING_LITERAL;
import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.AVERBATIM_STRING;
import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.CHAR_LITERAL;
import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.LINE_COMMENT;
import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.MULTI_COMMENT;
import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.STRING_END;
import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.STRING_LITERAL;
import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.STRING_MID;
import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.STRING_START;
import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.VERBATIM_STRING;
import static com.redhat.ceylon.eclipse.code.preferences.CeylonPreferenceInitializer.CLOSE_ANGLES;
import static com.redhat.ceylon.eclipse.code.preferences.CeylonPreferenceInitializer.CLOSE_BACKTICKS;
import static com.redhat.ceylon.eclipse.code.preferences.CeylonPreferenceInitializer.CLOSE_BRACES;
import static com.redhat.ceylon.eclipse.code.preferences.CeylonPreferenceInitializer.CLOSE_BRACKETS;
import static com.redhat.ceylon.eclipse.code.preferences.CeylonPreferenceInitializer.CLOSE_PARENS;
import static com.redhat.ceylon.eclipse.code.preferences.CeylonPreferenceInitializer.CLOSE_QUOTES;
import static com.redhat.ceylon.eclipse.util.Nodes.getTokenIndexAtCharacter;
import static com.redhat.ceylon.eclipse.util.Nodes.getTokenIterator;
import static java.lang.Character.isDigit;
import static java.lang.Character.isJavaIdentifierPart;
import static java.lang.Character.isLetter;
import static java.lang.Character.isUpperCase;
import static java.lang.Character.isWhitespace;
import static org.antlr.runtime.Token.HIDDEN_CHANNEL;
import static com.redhat.ceylon.eclipse.java2ceylon.Java2CeylonProxies.utilJ2C;
import java.util.Iterator;
import java.util.List;
import org.antlr.runtime.CommonToken;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentCommand;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer;
import com.redhat.ceylon.eclipse.ui.CeylonPlugin;
import com.redhat.ceylon.eclipse.util.Nodes;
class AutoEdit {
public AutoEdit(IDocument document,
List<CommonToken> tokens,
DocumentCommand command) {
this.document = document;
this.tokens = tokens;
this.command = command;
}
private IDocument document;
private List<CommonToken> tokens;
private DocumentCommand command;
//TODO: when pasting inside an existing string literal, should
// we automagically escape unescaped quotes in the pasted
// text?
public void customizeDocumentCommand() {
//Note that Correct Indentation sends us a tab
//character at the start of each line of selected
//text. This is amazingly sucky because it's very
//difficult to distinguish Correct Indentation from
//an actual typed tab.
//Note also that typed tabs are replaced with spaces
//before this method is called if the spacesfortabs
//setting is enabled.
if (command.text!=null) {
//command.length>0 means we are replacing or deleting text
if (command.length==0) {
if (command.text.isEmpty()) {
//workaround for a really annoying bug where we
//get sent "" instead of "\t" or " " by IMP
//reconstruct what we would have been sent
//without the bug
if (getIndentWithSpaces()) {
int overhang =
getPrefix().length() %
getIndentSpaces();
command.text =
getDefaultIndent()
.substring(overhang);
}
else {
command.text = "\t";
}
smartIndentOnKeypress();
}
else if (isLineEnding(command.text)) {
//a typed newline (might have length 1 or 2,
//depending on the platform)
smartIndentAfterNewline();
}
else if (command.text.length()==1 ||
//when spacesfortabs is enabled, we get
//sent spaces instead of a tab - the right
//number of spaces to take us to the next
//tab stop
getIndentWithSpaces() &&
isIndent(getPrefix())) {
//anything that might represent a single
//keypress or a Correct Indentation
smartIndentOnKeypress();
}
}
if (command.text!=null &&
command.text.length()==1 &&
command.length==0) {
closeOpening();
}
}
}
private String getDefaultIndent() {
return utilJ2C().indents().getDefaultIndent();
}
private int getIndentSpaces() {
return (int) utilJ2C().indents().getIndentSpaces();
}
private boolean getIndentWithSpaces() {
return utilJ2C().indents().getIndentWithSpaces();
}
private static String[][] FENCES = {
{ "'", "'", CLOSE_QUOTES },
{ "\"", "\"", CLOSE_QUOTES },
{ "`", "`", CLOSE_BACKTICKS },
{ "<", ">", CLOSE_ANGLES },
{ "(", ")", CLOSE_PARENS },
{ "[", "]", CLOSE_BRACKETS }};
private void closeOpening() {
if (isCommented(command.offset)) {
return;
}
boolean quoted = isQuoted(command.offset);
String current = command.text;
try {
// don't close fences after a backslash escape
// TODO: improve this, check the surrounding
// token type!
if (command.offset>0 &&
document.getChar(command.offset-1)=='\\') {
return;
}
// don't close fences before an identifier,
// literal or opening paren
if (!quoted &&
//there are special rules for < below
!current.equals("<")) {
int curr = command.offset;
while (curr<document.getLength()) {
char ch = document.getChar(curr++);
if (isLetter(ch) ||
isDigit(ch) ||
ch=='(') {
return;
}
if (ch=='"' &&
!"\"".equals(current) &&
!"`".equals(current)) {
return;
}
if (ch=='\'' &&
!"\'".equals(current)) {
return;
}
if (ch!=' ') {
break;
}
}
}
}
catch (BadLocationException e) {}
String opening = null;
String closing = null;
boolean found=false;
IPreferenceStore store = CeylonPlugin.getPreferences();
for (String[] type: FENCES) {
if (type[0].equals(current) ||
type[1].equals(current)) {
if (store==null ||
store.getBoolean(type[2])) {
opening = type[0];
closing = type[1];
found = true;
break;
}
}
}
if (found) {
if (current.equals(closing)) {
//typed character is a closing fence
try {
// skip one ahead if next char is already a closing fence
if (skipClosingFence(closing) &&
closeOpeningFence(opening, closing)) {
command.text = null;
command.shiftsCaret = false;
command.caretOffset = command.offset + 1;
return;
}
}
catch (BadLocationException e) {}
}
boolean isInterpolation =
isGraveAccentCharacterInStringLiteral(command.offset, opening);
boolean isDocLink =
isOpeningBracketInAnnotationStringLiteral(command.offset, opening);
if (current.equals(opening) &&
(isInterpolation || isDocLink || !quoted)) {
//typed character is an opening fence
if (isInterpolation || isDocLink ||
closeOpeningFence(opening, closing)) {
//add a closing fence
command.shiftsCaret = false;
command.caretOffset = command.offset + 1;
if (isInterpolation) {
try {
if (command.offset>1 &&
document.get(command.offset-1,1)
.equals("`") &&
!document.get(command.offset-2,1)
.equals("`")) {
command.text += "``";
}
}
catch (BadLocationException e) {}
}
else if (isDocLink) {
try {
if (command.offset>1 &&
document.get(command.offset-1,1)
.equals("[") &&
!document.get(command.offset-2,1)
.equals("]")) {
command.text += "]]";
}
}
catch (BadLocationException e) {}
}
else if (opening.equals("\"")) {
try {
if (command.offset<=1 ||
!document.get(command.offset-1,1)
.equals("\"")) {
command.text += closing;
}
else if (command.offset>1 &&
document.get(command.offset-2,2)
.equals("\"\"") &&
!(command.offset>2 &&
document.get(command.offset-3,1)
.equals("\""))) {
command.text += "\"\"\"";
}
}
catch (BadLocationException e) {}
}
else {
command.text += closing;
}
}
}
}
}
private boolean skipClosingFence(String closing)
throws BadLocationException {
char ch = document.getChar(command.offset);
return String.valueOf(ch).equals(closing);
}
private boolean closeOpeningFence(String opening, String closing) {
if (opening.equals("<")) {
// only close angle brackets if it's after
// an uppercase identifier or open fence
int currOffset = command.offset;
try {
//TODO: eat whitespace
char ch = document.getChar(currOffset-1);
if (ch=='{'||ch=='('||ch=='['||ch=='<'||ch==',') {
return !isJavaIdentifierPart(document.getChar(currOffset));
}
while (isJavaIdentifierPart(ch) &&
--currOffset>0) {
ch = document.getChar(currOffset-1);
}
return currOffset<command.offset &&
isUpperCase(document.getChar(currOffset));
}
catch (BadLocationException e) {
return false;
}
}
else {
if (opening.equals(closing)) {
return count(opening)%2==0;
}
else {
return count(opening)>=count(closing);
}
}
}
private String getPrefix() {
try {
int lineOffset =
getStartOfCurrentLine();
return document.get(lineOffset,
command.offset-lineOffset) +
command.text;
}
catch (BadLocationException e) {
return command.text;
}
}
private boolean isIndent(String text) {
if (!text.isEmpty() &&
text.length() % getIndentSpaces()==0) {
for (char c: text.toCharArray()) {
if (c!=' ') return false;
}
return true;
}
else {
return false;
}
}
private void smartIndentAfterNewline() {
if (command.offset==-1 ||
document.getLength()==0) {
return;
}
try {
//if (end > start) {
indentNewLine();
//}
}
catch (BadLocationException bleid ) {
bleid.printStackTrace();
}
}
private void smartIndentOnKeypress() {
if (command.offset==-1 ||
document.getLength()==0) {
return;
}
try {
adjustIndentOfCurrentLine();
}
catch (BadLocationException ble) {
ble.printStackTrace();
}
}
private boolean isQuoted(int offset) {
int tokenType =
getTokenTypeStrictlyContainingOffset(offset);
return isStringToken(tokenType);
}
private boolean isStringToken(int type) {
return type==STRING_LITERAL ||
type==STRING_MID ||
type==STRING_START ||
type==STRING_END ||
type==VERBATIM_STRING ||
type==ASTRING_LITERAL ||
type==AVERBATIM_STRING ||
type==MULTI_COMMENT ||
type==CHAR_LITERAL; //doesn't really belong here
}
private boolean isCommented(int offset) {
int tokenType =
getTokenTypeStrictlyContainingOffset(offset);
return isCommentToken(tokenType);
}
private boolean isQuotedOrCommented(int offset) {
int tokenType =
getTokenTypeStrictlyContainingOffset(offset);
return isQuoteOrCommentToken(tokenType);
}
private boolean isQuoteOrCommentToken(int type) {
return type==STRING_LITERAL ||
type==STRING_MID ||
type==STRING_START ||
type==STRING_END ||
type==VERBATIM_STRING ||
type==ASTRING_LITERAL ||
type==AVERBATIM_STRING ||
type==CHAR_LITERAL ||
type==LINE_COMMENT ||
type==MULTI_COMMENT;
}
private boolean isCommentToken(int type) {
return type==LINE_COMMENT ||
type==MULTI_COMMENT;
}
private boolean isGraveAccentCharacterInStringLiteral(int offset,
String fence) {
if ("`".equals(fence)) {
int type = getTokenTypeStrictlyContainingOffset(offset);
return type == STRING_LITERAL ||
type == STRING_START ||
type == STRING_END ||
type == STRING_MID;
}
else {
return false;
}
}
private boolean isOpeningBracketInAnnotationStringLiteral(int offset,
String fence) {
if ("[".equals(fence)) {
int type = getTokenTypeStrictlyContainingOffset(offset);
//damn, AutoEdit can now no longer
//distinguish annotation strings :(
return type == ASTRING_LITERAL ||
type == AVERBATIM_STRING;
}
else {
return false;
}
}
private boolean isInUnterminatedMultilineComment(int offset,
IDocument d) {
CommonToken token =
getTokenStrictlyContainingOffset(offset);
if (token==null) return false;
try {
return token.getType()==MULTI_COMMENT &&
!token.getText().endsWith("*/") &&
d.getLineOfOffset(offset)+1==token.getLine();
}
catch (BadLocationException e) {
return false;
}
}
private String getRelativeIndent(int offset) {
int indent = getStringOrCommentIndent(offset);
try {
IRegion lineInfo =
document.getLineInformationOfOffset(offset);
StringBuilder result = new StringBuilder();
int lineOffset = lineInfo.getOffset();
for (int i = lineOffset;
i<lineOffset+indent;
i++) {
char ch = document.getChar(i);
if (ch!=' ' && ch!='\t') {
return "";
}
}
for (int i = lineOffset+indent;;) {
char ch = document.getChar(i++);
if (ch==' '||ch=='\t') {
result.append(ch);
}
else {
break;
}
}
return result.toString();
}
catch (BadLocationException e) {
e.printStackTrace();
return "";
}
}
private int getTokenTypeStrictlyContainingOffset(int offset) {
CommonToken token =
getTokenStrictlyContainingOffset(offset);
return token==null ? -1 : token.getType();
}
private int getTokenTypeOfCharacterAtOffset(int offset) {
int tokenIndex =
getTokenIndexAtCharacter(tokens, offset);
if (tokenIndex>=0) {
CommonToken token = tokens.get(tokenIndex);
return token.getType();
}
return -1;
}
private CommonToken getTokenStrictlyContainingOffset(int offset) {
return Nodes.getTokenStrictlyContainingOffset(offset,
getTokens());
}
private int getStringOrCommentIndent(int offset) {
CommonToken tokenContainingOffset =
getTokenStrictlyContainingOffset(offset);
CommonToken token =
getStartOfStringToken(tokenContainingOffset);
if (token!=null) {
int type = token.getType();
int start = token.getCharPositionInLine();
if (token.getStartIndex()<offset) {
switch (type) {
case STRING_LITERAL:
case STRING_START:
case ASTRING_LITERAL:
return start+1;
case STRING_MID:
case STRING_END:
case MULTI_COMMENT:
return start+1;
//uncomment to get a bigger indent
// return start+3;
case VERBATIM_STRING:
case AVERBATIM_STRING:
return start+3;
}
}
}
return -1;
}
private CommonToken getStartOfStringToken(CommonToken token) {
if (token==null) {
return null;
}
int type = token.getType();
if (type==STRING_MID||type==STRING_END) {
while (type!=STRING_START) {
int index = token.getTokenIndex();
if (index==0) {
return null;
}
token = tokens.get(index-1);
type = token.getType();
}
}
return token;
}
private void adjustIndentOfCurrentLine()
throws BadLocationException {
char ch = command.text.charAt(0);
if (isQuotedOrCommented(command.offset)) {
if (ch=='\t' ||
getIndentWithSpaces() &&
isIndent(getPrefix())) {
fixIndentOfStringOrCommentContinuation();
}
}
else {
switch (ch) {
case '}':
case ')':
reduceIndentOfCurrentLine();
break;
case '\t':
case '{':
case '(':
fixIndentOfCurrentLine();
default:
//when spacesfortabs is enabled, and a tab is
//pressed, we get sent spaces instead of a tab
//we need this special case to "jump" the caret
//to the start of the code in the line
if (getIndentWithSpaces() &&
isIndent(getPrefix())) {
fixIndentOfCurrentLine();
}
}
adjustStringOrCommentIndentation();
}
}
private void adjustStringOrCommentIndentation()
throws BadLocationException {
CommonToken tok =
getTokenStrictlyContainingOffset(getEndOfCurrentLine());
if (tok!=null) {
int len = command.length;
String text = command.text;
if (isQuoteOrCommentToken(tok.getType()) &&
text!=null && text.length()<len) { //reduced indent of a quoted or commented token
String indent = document.get(command.offset, len);
int line = document.getLineOfOffset(tok.getStartIndex())+1;
int lastLine = document.getLineOfOffset(tok.getStopIndex());
while (line<=lastLine) {
int offset = document.getLineOffset(line);
if (document.get(offset, len).equals(indent)) {
document.replace(offset, len, text);
}
line++;
}
}
}
}
private void fixIndentOfStringOrCommentContinuation()
throws BadLocationException {
int endOfWs =
firstEndOfWhitespace(command.offset,
getEndOfCurrentLine());
if (endOfWs<0) return;
CommonToken tokenContainingOffset =
getTokenStrictlyContainingOffset(command.offset);
CommonToken token =
getStartOfStringToken(tokenContainingOffset);
int pos = command.offset - getStartOfCurrentLine();
int tokenIndent = token.getCharPositionInLine();
if (pos>tokenIndent) return;
StringBuilder indent = new StringBuilder();
int startOfTokenLine =
document.getLineOffset(token.getLine()-1);
String prefix =
document.get(startOfTokenLine+pos,
tokenIndent-pos);
for (int i=0; i<prefix.length(); i++) {
char ch = prefix.charAt(i);
indent.append(ch=='\t'?'\t':' ');
}
indent.append(getExtraIndent(token));
indent.append(getRelativeIndent(command.offset));
if (indent.length()>0) {
command.length = endOfWs-command.offset;
command.text = indent.toString();
}
}
private String getExtraIndent(CommonToken token) {
String extraIndent = "";
switch (token.getType()) {
case MULTI_COMMENT:
//uncomment to get a bigger indent
// if (document.getLineOfOffset(command.offset) <
// document.getLineOfOffset(token.getStopIndex())) {
// extraIndent=" ";
// }
// else {
extraIndent=" ";
// }
break;
case STRING_MID:
case STRING_END:
extraIndent=" ";
break;
case STRING_LITERAL:
case ASTRING_LITERAL:
case STRING_START:
extraIndent=" ";
break;
case VERBATIM_STRING:
case AVERBATIM_STRING:
extraIndent=" ";
break;
}
return extraIndent;
}
private void indentNewLine()
throws BadLocationException {
int stringIndent =
getStringOrCommentIndent(command.offset);
int start = getStartOfCurrentLine();
if (stringIndent>=0) {
//we're in a string or multiline comment
StringBuilder sb = new StringBuilder();
for (int i=0; i<stringIndent; i++) {
char ws = document.getChar(start+i)=='\t' ?
'\t' : ' ';
sb.append(ws);
}
command.text = command.text +
sb.toString() +
getRelativeIndent(command.offset);
}
else {
char endOfLastLineChar =
getPreviousNonHiddenCharacterInLine(command.offset);
char startOfNewLineChar =
getNextNonHiddenCharacterInNewline(command.offset);
StringBuilder buf =
new StringBuilder(command.text);
IPreferenceStore store = CeylonPlugin.getPreferences();
boolean closeBrace = store==null ||
store.getBoolean(CLOSE_BRACES);
int end = getEndOfCurrentLine();
appendIndent(command.offset, end, start,
command.offset,
startOfNewLineChar, endOfLastLineChar,
closeBrace, buf);
if (buf.length()>2) {
char ch = buf.charAt(buf.length()-1);
if (ch=='}'||ch==')') {
String hanging =
document.get(command.offset,
end-command.offset); //stuff after the { on the current line
buf.insert(command.caretOffset-command.offset, hanging);
command.length = hanging.length();
}
}
command.text = buf.toString();
}
closeUnterminatedMultlineComment();
}
private void closeUnterminatedMultlineComment() {
if (isInUnterminatedMultilineComment(command.offset, document)) {
command.shiftsCaret=false;
String text = command.text;
command.caretOffset=command.offset+text.length();
command.text = text +
text +
//uncomment to get a bigger indent
// (text.indexOf(' ')>=0 ?
// text.replaceFirst(" ", "") : text) +
"*/";
}
}
private int count(String token) {
int count = 0;
List<CommonToken> tokens = getTokens();
for (CommonToken tok: tokens) {
String text = tok.getText();
if (text.equals(token)) {
count++;
}
if (text.startsWith(token) &&
!text.endsWith(token)) {
count++;
}
if (text.endsWith(token) &&
!text.startsWith(token)) {
count++;
}
}
return count;
}
private int count(String token, int startIndex, int stopIndex) {
int count = 0;
List<CommonToken> tokens = getTokens();
for (CommonToken tok: tokens) {
if (tok.getStartIndex()>=startIndex &&
tok.getStopIndex()<stopIndex &&
tok.getText().equals(token)) {
count++;
}
}
return count;
}
private List<CommonToken> getTokens() {
return tokens;
}
private void fixIndentOfCurrentLine()
throws BadLocationException {
int start = getStartOfCurrentLine();
int end = getEndOfCurrentLine();
int endOfWs = firstEndOfWhitespace(start, end);
// we want this to happen in three cases:
// 1. the user types a tab in the whitespace
// at the start of the line
// 2. the user types { or ( at the start of
// the line
// 3. Correct Indentation is calling us
//test for Correct Indentation action
boolean correctingIndentation =
command.offset==start &&
!command.shiftsCaret;
boolean opening =
command.text.equals("{") ||
command.text.equals("(");
if (command.offset<endOfWs || //we want strictly < since we don't want to prevent the caret advancing when a space is typed
command.offset==endOfWs && endOfWs==end && opening || //this can cause the caret to jump *backwards* when a { or ( is typed!
correctingIndentation) {
int endOfPrev = getEndOfPreviousLine();
int startOfPrev = getStartOfPreviousLine();
char startOfCurrentLineChar =
opening ?
command.text.charAt(0) : //the typed character is now the first character in the line
getNextNonHiddenCharacterInLine(start);
char endOfLastLineChar =
getPreviousNonHiddenCharacterInLine(endOfPrev);
StringBuilder buf = new StringBuilder();
appendIndent(start, end,
startOfPrev, endOfPrev,
startOfCurrentLineChar, endOfLastLineChar,
false, buf);
int len = endOfWs-start;
String text = buf.toString();
if (text.length()!=len ||
!document.get(start,len).equals(text)) {
if (opening) {
text+=command.text;
}
command.text = text;
command.offset = start;
command.length = len;
}
else if (!opening) {
command.caretOffset = start+len;
command.shiftsCaret = false;
command.text = null;
}
}
}
private void appendIndent(int startOfCurrent,
int endOfCurrent,
int startOfPrev, int endOfPrev,
char startOfCurrentLineChar,
char endOfLastLineChar,
boolean closeBraces,
StringBuilder buf)
throws BadLocationException {
CommonToken prevEnding =
getPreviousNonHiddenToken(endOfPrev);
CommonToken currStarting =
getNextNonHiddenToken(startOfCurrent,
endOfCurrent);
boolean terminatedCleanly =
endOfLastLineChar==';' ||
endOfLastLineChar==',';
boolean isContinuation = !terminatedCleanly &&
//note: unfortunately we can't treat a line after a closing paren
// as a continuation because it might be an annotation
(isBinaryOperator(prevEnding) ||
isBinaryOperator(currStarting) ||
isInheritanceClause(currStarting) ||
isOperatorChar(startOfCurrentLineChar)); //to account for a previously line-commented character
boolean isClosing =
startOfCurrentLineChar=='}' /*&& endOfLastLineChar!='{'*/ ||
startOfCurrentLineChar==')' /*&& endOfLastLineChar!='('*/;
boolean isOpening =
endOfLastLineChar=='{' /*&& startOfCurrentLineChar!='}'*/ ||
endOfLastLineChar=='(' /*&& startOfCurrentLineChar!=')'*/;
boolean isListContinuation =
count("{",startOfPrev,endOfPrev) >
count("}",startOfPrev,endOfPrev) ||
count("(",startOfPrev,endOfPrev) >
count(")",startOfPrev,endOfPrev);
appendIndent(isContinuation,
isOpening, isClosing,
isListContinuation,
startOfPrev, endOfPrev,
closeBraces, buf);
}
private boolean isInheritanceClause(CommonToken t) {
if (t==null) return false;
int tt = t.getType();
return tt==CeylonLexer.EXTENDS ||
tt==CeylonLexer.CASE_TYPES ||
tt==CeylonLexer.TYPE_CONSTRAINT ||
tt==CeylonLexer.SATISFIES;
}
private boolean isOperatorChar(char ch) {
return ch=='+'||
ch=='-'||
ch=='/'||
ch=='*'||
ch=='^'||
ch=='%'||
ch=='|'||
ch=='&'||
ch=='='||
ch=='<'||
ch=='>'||
ch=='~'||
ch=='?'||
ch=='.'||
ch=='!';
}
private boolean isBinaryOperator(CommonToken t) {
if (t==null) return false;
int tt = t.getType();
//partial fix for #1253
/*if (tt==CeylonLexer.ELSE_CLAUSE ||
tt==CeylonLexer.THEN_CLAUSE) {
CommonToken nextToken =
getNextNonHiddenToken(t.getStopIndex(),
Integer.MAX_VALUE);
return nextToken!=null &&
nextToken.getType()!=CeylonLexer.LBRACE &&
nextToken.getType()!=CeylonLexer.IF_CLAUSE;
}*/
return tt==CeylonLexer.SPECIFY ||
tt==CeylonLexer.COMPUTE ||
tt==CeylonLexer.NOT_EQUAL_OP ||
tt==CeylonLexer.EQUAL_OP ||
tt==CeylonLexer.IDENTICAL_OP ||
tt==CeylonLexer.ADD_SPECIFY ||
tt==CeylonLexer.SUBTRACT_SPECIFY ||
tt==CeylonLexer.DIVIDE_SPECIFY ||
tt==CeylonLexer.MULTIPLY_SPECIFY ||
tt==CeylonLexer.OR_SPECIFY ||
tt==CeylonLexer.AND_SPECIFY ||
tt==CeylonLexer.COMPLEMENT_SPECIFY ||
tt==CeylonLexer.UNION_SPECIFY ||
tt==CeylonLexer.INTERSECT_SPECIFY ||
tt==CeylonLexer.MEMBER_OP ||
tt==CeylonLexer.SPREAD_OP ||
tt==CeylonLexer.SAFE_MEMBER_OP ||
tt==CeylonLexer.SUM_OP ||
tt==CeylonLexer.COMPLEMENT_OP ||
tt==CeylonLexer.DIFFERENCE_OP ||
tt==CeylonLexer.QUOTIENT_OP ||
tt==CeylonLexer.PRODUCT_OP ||
tt==CeylonLexer.REMAINDER_OP ||
tt==CeylonLexer.RANGE_OP ||
tt==CeylonLexer.SEGMENT_OP ||
tt==CeylonLexer.ENTRY_OP ||
tt==CeylonLexer.UNION_OP ||
tt==CeylonLexer.INTERSECTION_OP ||
tt==CeylonLexer.AND_OP ||
tt==CeylonLexer.OR_OP ||
tt==CeylonLexer.POWER_OP ||
tt==CeylonLexer.COMPARE_OP ||
tt==CeylonLexer.LARGE_AS_OP ||
tt==CeylonLexer.LARGER_OP ||
tt==CeylonLexer.SMALL_AS_OP ||
tt==CeylonLexer.SMALLER_OP ||
tt==CeylonLexer.SCALE_OP;
}
// private void reduceIndent(DocumentCommand command) {
// int spaces = getIndentSpaces();
// if (endsWithSpaces(command.text, spaces)) {
// command.text = command.text.substring(0, command.text.length()-spaces);
// }
// else if (command.text.endsWith("\t")) {
// command.text = command.text.substring(0, command.text.length()-1);
// }
// }
private void reduceIndentOfCurrentLine()
throws BadLocationException {
int spaces = getIndentSpaces();
String text =
document.get(command.offset-spaces, spaces);
if (endsWithSpaces(text,spaces)) {
command.offset = command.offset-spaces;
command.length = spaces;
}
else if (document.get(command.offset-1,1).equals("\t")) {
command.offset = command.offset-1;
command.length = 1;
}
}
private void decrementIndent(StringBuilder buf, String indent)
throws BadLocationException {
int spaces = getIndentSpaces();
if (endsWithSpaces(indent,spaces)) {
buf.setLength(buf.length()-spaces);
}
else if (endsWithTab(indent)) {
buf.setLength(buf.length()-1);
}
}
/*private int getStartOfNextLine(IDocument d, int offset)
throws BadLocationException {
return d.getLineOffset(d.getLineOfOffset(offset)+1);
}*/
private void appendIndent(boolean isContinuation,
boolean isOpening, boolean isClosing,
boolean isListContinuation,
int start, int end,
boolean closeBraces,
StringBuilder buf)
throws BadLocationException {
int line = document.getLineOfOffset(start);
String indent = getIndent(start, end);
String delim = getLineDelimiter(document, line);
buf.append(indent);
if (isOpening||isListContinuation) {
if (isClosing) {
if (closeBraces && isOpening) {
//increment the indent level
incrementIndent(buf, indent);
//move the closing brace to next line
command.shiftsCaret = false;
command.caretOffset =
command.offset+buf.length();
buf.append(delim)
.append(indent);
}
// else if (closeBraces && //hack just to distinguish a newline from a correct indentation!
// isListContinuation) {
// //just increment the indent level
// incrementIndent(buf, indent);
// }
}
else {
//increment the indent level
incrementIndent(buf, indent);
if (closeBraces &&
count("{") > count("}")) {
//close the opening brace
command.shiftsCaret = false;
command.caretOffset =
command.offset+buf.length();
buf.append(delim)
.append(indent)
.append('}');
}
}
}
else if (isContinuation) {
incrementIndent(buf, indent);
incrementIndent(buf, indent);
}
else if (isClosing) {
decrementIndent(buf, indent);
}
}
private static String getLineDelimiter(IDocument document, int line)
throws BadLocationException {
String newlineChar =
document.getLineDelimiter(line);
if (newlineChar==null && line>0) {
return document.getLineDelimiter(line-1);
}
else {
return getDefaultLineDelimiter(document);
}
}
private static String getDefaultLineDelimiter(IDocument document) {
return utilJ2C().indents().getDefaultLineDelimiter(document);
}
private String getIndent(int start, int end)
throws BadLocationException {
if (start<0||end<0) return "";
int nestingLevel = 0;
while (start>0) {
nestingLevel += parenCount(start, end);
//We're searching for an earlier line whose
//immediately preceding line ends cleanly
//with a {, }, or ; or which itelf starts
//with a }. We will use that to infer the
//indent for the current line
char startingChar =
getNextNonHiddenCharacterInLine(start);
if (startingChar=='}' || startingChar==')') break;
int prevEnd = end;
int prevStart = start;
prevEnd = getEndOfPreviousLine(prevStart);
prevStart = getStartOfPreviousLine(prevStart);
char prevEndingChar =
getPreviousNonHiddenCharacterInLine(prevEnd);
if (prevEndingChar==';' ||
prevEndingChar==',' && nestingLevel>=0 ||
prevEndingChar=='{' ||
prevEndingChar=='}' ||
prevEndingChar=='(' && nestingLevel>=0)
//note assymmetry between } and ) here,
//due to stuff like "class X()\nextends Y()"
//and X f()\n=> X()
break;
end = prevEnd;
start = prevStart;
}
while (isQuoted(start)) {
end = getEndOfPreviousLine(start);
start = getStartOfPreviousLine(start);
}
int len = firstEndOfWhitespace(start, end) - start;
return document.get(start, len);
}
private int parenCount(int start, int end) {
int count=0;
for (Iterator<CommonToken> it =
getTokenIterator(getTokens(),
new Region(start, end-start));
it.hasNext();) {
int type = it.next().getType();
if (type==CeylonLexer.RPAREN) {
count--;
}
else if (type==CeylonLexer.LPAREN) {
count++;
}
}
return count;
}
private void incrementIndent(StringBuilder buf,
String indent) {
int spaces = getIndentSpaces();
if (endsWithSpaces(indent,spaces)) {
for (int i=1; i<=spaces; i++) {
buf.append(' ');
}
}
else if (endsWithTab(indent)) {
buf.append('\t');
}
else {
initialIndent(buf);
}
}
private void initialIndent(StringBuilder buf) {
utilJ2C().indents().initialIndent(buf);
}
private boolean endsWithTab(String indent) {
return !indent.isEmpty() &&
indent.charAt(indent.length()-1)=='\t';
}
private CommonToken getPreviousNonHiddenToken(int offset) {
int index =
getTokenIndexAtCharacter(tokens, offset);
if (index<0) index=-index;
for (; index>=0; index--) {
CommonToken token = getTokens().get(index);
if (token.getChannel()!=HIDDEN_CHANNEL &&
token.getStopIndex()<offset) {
return token;
}
}
return null;
}
private CommonToken getNextNonHiddenToken(int offset, int end) {
int index =
getTokenIndexAtCharacter(tokens, offset);
if (index<0) index=1-index;
int size = getTokens().size();
for (; index<size; index++) {
CommonToken token = getTokens().get(index);
if (token.getStartIndex()>=end) {
return null;
}
if (token.getChannel()!=HIDDEN_CHANNEL &&
token.getStartIndex()>=offset) {
return token;
}
}
return null;
}
/**
* Is the given offset in the document a line ending?
*/
private boolean isLineEnding(int offset) {
int documentLength = document.getLength();
String[] delimiters =
document.getLegalLineDelimiters();
for (String delimiter: delimiters) {
int length = delimiter.length();
if (offset+length <= documentLength) {
try {
String string =
document.get(offset,length);
if (string.equals(delimiter)) {
return true;
}
}
catch (BadLocationException e) {
e.printStackTrace();
}
}
}
return false;
}
private char getPreviousNonHiddenCharacterInLine(int offset)
throws BadLocationException {
offset--;
for (; offset>=0; offset--) {
char ch = document.getChar(offset);
int tt = getTokenTypeOfCharacterAtOffset(offset);
if (!isWhitespace(ch) && !isCommentToken(tt) ||
isLineEnding(offset)) {
return ch;
}
}
return '\n'; //lame null
}
private char getNextNonHiddenCharacterInLine(int offset)
throws BadLocationException {
for (; offset<document.getLength(); offset++) {
char ch = document.getChar(offset);
int tt = getTokenTypeOfCharacterAtOffset(offset);
if (!isWhitespace(ch) && !isCommentToken(tt) ||
isLineEnding(offset)) {
return ch;
}
}
return '\n'; //lame null
}
private char getNextNonHiddenCharacterInNewline(int offset)
throws BadLocationException {
for (; offset<document.getLength(); offset++) {
char ch = document.getChar(offset);
try {
if (document.get(offset,2).equals("//")) {
break;
}
}
catch (BadLocationException ble) {}
int tt = getTokenTypeOfCharacterAtOffset(offset);
if (!isWhitespace(ch) && tt!=MULTI_COMMENT ||
isLineEnding(offset)) {
return ch;
}
}
return '\n'; //lame null
}
private int getStartOfCurrentLine()
throws BadLocationException {
int p = command.offset == document.getLength() ?
command.offset-1 : command.offset;
IRegion lineInfo =
document.getLineInformationOfOffset(p);
return lineInfo.getOffset();
}
private int getEndOfCurrentLine()
throws BadLocationException {
int p = command.offset == document.getLength() ?
command.offset-1 : command.offset;
IRegion lineInfo =
document.getLineInformationOfOffset(p);
return lineInfo.getOffset() + lineInfo.getLength();
}
private int getStartOfPreviousLine()
throws BadLocationException {
return getStartOfPreviousLine(command.offset);
}
private int getStartOfPreviousLine(int offset)
throws BadLocationException {
int line = document.getLineOfOffset(offset);
IRegion lineInfo;
do {
if (line==0) return 0;
lineInfo = document.getLineInformation(--line);
}
while (lineInfo.getLength()==0 ||
isQuoted(lineInfo.getOffset()));
return lineInfo.getOffset();
}
private int getEndOfPreviousLine()
throws BadLocationException {
return getEndOfPreviousLine(command.offset);
}
private int getEndOfPreviousLine(int offset)
throws BadLocationException {
if (offset == document.getLength() && offset>0) {
offset--;
}
int line = document.getLineOfOffset(offset);
IRegion lineInfo;
do {
if (line==0) return 0;
lineInfo = document.getLineInformation(--line);
}
while (lineInfo.getLength()==0);
return lineInfo.getOffset() +
lineInfo.getLength();
}
private boolean endsWithSpaces(String string, int spaces) {
if (string.length()<spaces) return false;
for (int i=1; i<=spaces; i++) {
if (string.charAt(string.length()-i)!=' ') {
return false;
}
}
return true;
}
/**
* Returns the first offset greater than <code>offset</code>
* and smaller than <code>end</code> whose character is
* not a space or tab character. If no such offset is
* found, <code>end</code> is returned.
*
* @param d the document to search in
* @param offset the offset at which searching start
* @param end the offset at which searching stops
* @return the offset in the specified range whose
* character is not a space or tab
* @exception BadLocationException if position is an
* invalid range in the given document
*/
private int firstEndOfWhitespace(int offset, int end)
throws BadLocationException {
while (offset < end) {
char ch= document.getChar(offset);
if (ch!=' ' && ch!='\t') {
return offset;
}
offset++;
}
return end;
}
/**
* Is the given character sequence a line-ending
* character sequence for this document/platform?
*/
private boolean isLineEnding(String text) {
String[] delimiters =
document.getLegalLineDelimiters();
for (String delim: delimiters) {
if (delim.equals(text)) {
return true;
}
}
return false;
}
}