/* * Copyright 2009-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.codehaus.groovy.eclipse.core.util; import org.codehaus.groovy.eclipse.core.ISourceBuffer; import org.codehaus.groovy.eclipse.core.impl.StringSourceBuffer; /** * An expression finder. Used to find expressions that are suitable for content assist. * <p> * Examples are: * <ul> * <li>hello</li> * <li>thing.value</li> * <li>thing[10].value</li> * <li>[1, 2, 3].collect { it.toString() }. // Note the '.'</li> * </ul> */ public class ExpressionFinder { /** * Find an expression starting at the offset and working backwards. * The found expression is one that could possibly have completions. * * @return The expression, or null if no suitable expression was found. */ public String findForCompletions(ISourceBuffer sourceBuffer, int offset) throws ParseException { Token token = null; int endOffset = 0; TokenStream stream = new TokenStream(sourceBuffer, offset); try { token = stream.peek(); if (token == null || token.isType(Token.Type.EOF)) { return null; } endOffset = token.endOffset; boolean offsetIsWhitespace = Character.isWhitespace(stream.getCurrentChar()); boolean offsetIsQuote = stream.getCurrentChar() == '\"' || stream.getCurrentChar() == '\''; // no expression associated with a quote if (offsetIsQuote) { return null; } skipLineBreaksAndComments(stream); token = stream.next(); // if the offset is a whitespace, then content assist should be on a blank expression unless there is a '.', '..', '?.', '*.', '.@', or '.&' if (offsetIsWhitespace && !token.isDotAccess() && !token.isType(Token.Type.DOUBLE_DOT)) { return ""; } if ("@".equals(token.text)) { return "@"; } if (token.isType(Token.Type.EOF)) { return null; } switch (token.getType()) { case DOT: case DOUBLE_DOT: case SAFE_DEREF: case SPREAD: case FIELD_ACCESS: case METHOD_POINTER: token = dot(stream); break; case IDENT: token = ident(stream); break; case BRACK_BLOCK: token = null; break; default: throw new ParseException(token); } } catch (TokenStreamException e) { // FUTURE: emp - the token stream should return EOF, for tokens [ { ( etc. or the tokens themselves. // This can happen: if () { a._ // as '{' is unexpected without '}' - there are no tokens for the block delimiters. // Because of this exception, the last token has not been returned. Patch that here. Token last = stream.last(); if (last != null) { token = last; } } catch (IllegalStateException e) { } if (token != null) { return sourceBuffer.subSequence(token.startOffset, endOffset).toString(); } return ""; } /** * Finds the end of the String token that exists at initialOffset. * searches the document for the next non-word character and returns that * as the end * * @param buffer the document to search * @param initialOffset the initial offset * @return the offset of the first non-word character starting at initialOffset */ public int findTokenEnd(ISourceBuffer buffer, int initialOffset) { int candidate = initialOffset; while (buffer.length() > candidate) { if (!Character.isJavaIdentifierPart(buffer.charAt(candidate))) { break; } candidate += 1; } return candidate; } /** * Splits the given expression into two parts: the type evaluation part, and * the code completion part. * * @param expression returned by the {@link #findForCompletions(ISourceBuffer, int)} method * @return A string pair, the expression to complete, and the prefix to be * completed.<br> * { "", null } if no completion expression could be found * String[0] is an expression .<br> * String[1] is the empty string if the last character is a '.'.<br> * String[1] is 'ident' if the expression ends with '.ident'.<br> * String[1] is null if the expression itself is to be used for * completion. * Also, remove starting '$'. These only occur when inside GStrings, * and should not be completed against. */ public String[] splitForCompletion(String expression) { String[] split = splitForCompletionNoTrim(expression); if (split[0] != null) { split[0] = split[0].trim(); if (split[0].startsWith("$")) { split[0] = split[0].substring(1); } } if (split[1] != null) { split[1] = split[1].trim(); if (split[1].startsWith("$")) { split[1] = split[1].substring(1); } } return split; } public String[] splitForCompletionNoTrim(String expression) { String[] ret = new String[2]; if (expression == null || expression.trim().length() < 1 ){ ret[0] = ""; ret[1] = null; return ret; } StringSourceBuffer sb = new StringSourceBuffer(expression); TokenStream stream = new TokenStream(sb, expression.length() - 1); Token token0, token1, token2; try { skipLineBreaksAndComments(stream); token0 = stream.next(); skipLineBreaksAndComments(stream); token1 = stream.next(); skipLineBreaksAndComments(stream); token2 = stream.next(); if (token0.isDotAccess() && token1.isValidBeforeDot()) { ret[0] = expression.substring(0, token1.endOffset); ret[1] = ""; } else if (token0.isType(Token.Type.IDENT) && token1.isDotAccess() && token2.isValidBeforeDot()) { ret[0] = expression.substring(0, token2.endOffset); ret[1] = expression.substring(token0.startOffset, expression.length()); } else if (token0.isType(Token.Type.IDENT)) { ret[0] = expression; } else { ret = new String[] { "", null }; } } catch (TokenStreamException e) { ret = new String[] { "", null }; } catch (IllegalStateException e) { ret = new String[] { "", null }; } return ret; } public static class NameAndLocation { public final String name; public final int location; public NameAndLocation(String name, int locaiton) { this.name = name; this.location = locaiton; } public String toTypeName() { StringBuilder sb = new StringBuilder(); int i = 0; while (i < name.length() && Character.isJavaIdentifierPart(name.charAt(i))) { sb.append(name.charAt(i++)); } return sb.toString(); } public int dims() { int i = 0; int dims = 0; while (i < name.length()) { if (name.charAt(i++) == ']') { dims += 1; } } return dims; } } public NameAndLocation findPreviousTypeNameToken(ISourceBuffer buffer, int start) { int current = Math.min(start, buffer.length()) - 1; while (current >= 0 && !Character.isWhitespace(buffer.charAt(current)) && Character.isJavaIdentifierPart(buffer.charAt(current))) { current -= 1; } if (current < 0 || !Character.isWhitespace(buffer.charAt(current))) { return null; } // don't allow newline chars, but do allow [] and whitespace StringBuilder sb = new StringBuilder(); while (current >= 0 && (Character.isWhitespace(buffer.charAt(current)) || buffer.charAt(current) == '[' || buffer.charAt(current) == ']') && buffer.charAt(current) != '\n' && buffer.charAt(current) != '\r') { // current--; sb.append(buffer.charAt(current--)); } if (current < 0 || !Character.isJavaIdentifierPart(buffer.charAt(current))) { return null; } while (current >= 0 && Character.isJavaIdentifierPart(buffer.charAt(current))) { sb.append(buffer.charAt(current--)); } if (sb.length() > 0) { return new NameAndLocation(sb.reverse().toString(), current + 1); } else { return null; } } /** * FIXADE only skip line breaks if the previous character is a '.' otherwise * line breaks should signify the end of the completion. * For now, though we just ignore skipping all line breaks * */ private void skipLineBreaksAndComments(TokenStream stream) throws TokenStreamException { skipLineBreaks(stream); skipLineComments(stream); } private Token dot(TokenStream stream) throws TokenStreamException, ParseException { skipLineBreaksAndComments(stream); Token token = stream.next(); switch (token.getType()) { case IDENT: return ident(stream); case QUOTED_STRING: return quotedString(stream); case PAREN_BLOCK: return parenBlock(stream); case BRACE_BLOCK: return braceBlock(stream); case BRACK_BLOCK: return brackBlock(stream); default: throw new ParseException(token); } } private void skipLineComments(TokenStream stream) throws TokenStreamException { while (stream.peek().isType(Token.Type.LINE_COMMENT)) { stream.next(); } } private void skipLineBreaks(TokenStream stream) throws TokenStreamException { while (stream.peek().isType(Token.Type.LINE_BREAK)) { stream.next(); } } private Token ident(TokenStream stream) throws TokenStreamException, ParseException { Token token = stream.peek(); Token last = stream.last(); switch (token.getType()) { case LINE_BREAK: skipLineBreaksAndComments(stream); token = stream.peek(); if (!token.isDotAccess()) { return new Token(Token.Type.EOF, last.startOffset, last.endOffset, null); } stream.next(); return dot(stream); case DOUBLE_DOT: return new Token(Token.Type.EOF, last.startOffset, last.endOffset, null); case METHOD_POINTER: case FIELD_ACCESS: case SAFE_DEREF: case SPREAD: case DOT: { stream.next(); return dot(stream); } // Anything that is not a dot before an ident is assumed to be EOF, unless it is the 'new' keyword. // This is because, a previous line of code can end with ) ] } ident ; etc. case IDENT: // A 'new' keyword is the beginning of the expression to find. if (token.text.equals("new")) { Token next = stream.next(); return new Token(Token.Type.EOF, next.startOffset, next.endOffset, null); } // fall through default: return new Token(Token.Type.EOF, last.startOffset, last.endOffset, null); } } private Token quotedString(TokenStream stream) throws TokenStreamException, ParseException { Token token = stream.peek(); Token last; switch (token.getType()) { case EOF: case LINE_BREAK: last = stream.last(); return new Token(Token.Type.EOF, last.startOffset, last.startOffset, null); case SEMI: last = stream.last(); return new Token(Token.Type.EOF, last.startOffset, last.startOffset, null); case IDENT: last = stream.last(); return new Token(Token.Type.EOF, last.startOffset, last.startOffset, null); default: throw new ParseException(token); } } private Token parenBlock(TokenStream stream) throws TokenStreamException, ParseException { Token token = stream.peek(); switch (token.getType()) { case IDENT: stream.next(); return ident(stream); case EOF: case SEMI: case LINE_BREAK: // expression in paren return stream.last(); default: throw new ParseException(token); } } private Token braceBlock(TokenStream stream) throws TokenStreamException, ParseException { Token token = stream.next(); switch (token.getType()) { case IDENT: return ident(stream); case PAREN_BLOCK: return parenBlock(stream); default: throw new ParseException(token); } } private Token brackBlock(TokenStream stream) throws TokenStreamException, ParseException { Token last = stream.last(); Token token = stream.next(); switch (token.getType()) { case EOF: return new Token(Token.Type.EOF, last.startOffset, last.startOffset, null); case IDENT: return ident(stream); case PAREN_BLOCK: return parenBlock(stream); case BRACE_BLOCK: return braceBlock(stream); case BRACK_BLOCK: return brackBlock(stream); case SEMI: case LINE_BREAK: // expression in paren return stream.last(); default: throw new ParseException(token); } } }