bear.plugins.groovy.GroovyCodeCompleter.java Source code

Java tutorial

Introduction

Here is the source code for bear.plugins.groovy.GroovyCodeCompleter.java

Source

/*
 * Copyright (C) 2013 Andrey Chaschev.
 *
 * 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 bear.plugins.groovy;

//import antlr.ASTFactory;
//import antlr.collections.AST;

import chaschev.lang.OpenBean;
import chaschev.lang.reflect.MethodDesc;
import com.google.common.collect.Lists;
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.groovy.antlr.SourceBuffer;
import org.codehaus.groovy.antlr.UnicodeEscapingReader;
import org.codehaus.groovy.antlr.parser.GroovyLexer;

import java.io.StringReader;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;

import static chaschev.lang.OpenBean.getFieldValue;
import static java.lang.Character.isWhitespace;

/**
 * @author Andrey Chaschev chaschev@gmail.com
 */
public class GroovyCodeCompleter {
    Binding binding;
    GroovyShell shell;

    static class Candidate implements Comparable<Candidate> {
        Replacement r;
        int score;

        Candidate(Replacement r, int score) {
            this.r = r;
            this.score = score;
        }

        @Override
        public int compareTo(Candidate o) {
            return -Integer.compare(score, o.score);
        }
    }

    static class Token {
        String name;
        boolean method;

        int start;
        int end = -1;

        Token(int start) {
            this.start = start;
        }

        Token(String name, boolean method) {
            this.name = name;
            this.method = method;
        }

        static Token m(String name) {
            return new Token(name, true);
        }

        public String text(String s) {
            return s.substring(start, end);
        }

        static Token f(String name) {
            return new Token(name, false);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            Token token = (Token) o;

            if (method != token.method)
                return false;
            if (name != null ? !name.equals(token.name) : token.name != null)
                return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = name != null ? name.hashCode() : 0;
            result = 31 * result + (method ? 1 : 0);
            return result;
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("Token{");
            sb.append("name='").append(name).append('\'');
            sb.append(", method=").append(method);
            sb.append(", start=").append(start);
            sb.append(", end=").append(end);
            sb.append('}');
            return sb.toString();
        }
    }

    public GroovyCodeCompleter(GroovyShell shell) {
        this.shell = shell;
        this.binding = (Binding) getFieldValue(shell, "context");
    }

    public GroovyCodeCompleter(Binding binding, GroovyShell shell) {
        this.binding = binding;
        this.shell = shell;
    }

    public Replacements completeCode(String script, int position) {
        int[] range = scanForStart(script, position, -1);

        int start = range[0];
        int end = range[1] + 1;

        boolean endsWithDot = script.charAt(firstNonSpace(script, Math.min(end, script.length() - 1), -1)) == '.';

        List<Token> tokens = tokenize(script, start, end);

        //fo| => size 1
        //foo. => size 1
        Class<?> firstTokenClass;
        if (tokens.size() == 1 && !endsWithDot) {
            firstTokenClass = null;
            //match vars from binding, there should be a few

            Set<Map.Entry<String, ?>> entries = binding.getVariables().entrySet();
            List<Candidate> candidates = new ArrayList<Candidate>();

            for (Map.Entry<String, ?> entry : entries) {
                String varName = entry.getKey();

                char[] chars = tokens.get(0).name.toCharArray();

                int score = 0;

                for (int i = 0; i < chars.length; i++) {
                    score += frequency(varName, chars[i]) * i == 0 ? 3 : (i == 1 ? 2 : 1);
                }

                if (score > 0) {
                    candidates.add(new Candidate(
                            new Replacement(varName, entry.getValue().getClass().getSimpleName()), score));
                }
            }

            Collections.sort(candidates);

            return new Replacements(start, end).addAll(candidates);
        } else {
            Object variable = shell.getVariable(tokens.get(0).name);
            firstTokenClass = variable == null ? null : variable.getClass();
        }

        List<Class<?>> currentClasses = firstTokenClass == null ? new ArrayList<Class<?>>()
                : Lists.<Class<?>>newArrayList(firstTokenClass);

        for (int i = 1; i < tokens.size(); i++) {
            Token token = tokens.get(i);
            boolean lastToken = i == tokens.size() - 1;

            if (lastToken && !endsWithDot) {
                break;
            }

            //strict match
            List<Class<?>> returnTypes = new ArrayList<Class<?>>();

            if (token.method) {
                for (Class<?> currentClass : currentClasses) {
                    for (MethodDesc method : OpenBean.methods(currentClass)) {
                        if (method.getName().equals(token.name)) {
                            returnTypes.add(method.getMethod().getReturnType());
                        }
                    }
                }
            } else {
                for (Class<?> currentClass : currentClasses) {
                    for (Field field : OpenBean.fields(currentClass)) {
                        if (field.getName().equals(token.name)) {
                            returnTypes.add(field.getType());
                        }
                    }
                }
            }

            if (returnTypes.size() > 1) {
                currentClasses = Lists.newArrayList(new LinkedHashSet<Class<?>>(returnTypes));
            } else {
                currentClasses = returnTypes;
            }
        }

        String pattern = null;

        if (!endsWithDot) {
            pattern = tokens.get(tokens.size() - 1).name.toLowerCase();
        }

        Replacements replacements = endsWithDot ? new Replacements(position, position)
                : new Replacements(tokens.get(tokens.size() - 1).start, position);

        if (endsWithDot) {
            for (Class<?> currentClass : currentClasses) {
                for (Field field : OpenBean.fields(currentClass)) {
                    replacements.add(new Replacement(field));
                }

                for (MethodDesc method : OpenBean.methods(currentClass)) {
                    replacements.add(new Replacement(method));
                }
            }
        } else {
            final String patternLC = pattern.toLowerCase();
            Set<Field> usedFields = new HashSet<Field>();
            Set<Method> usedMethods = new HashSet<Method>();

            List<Candidate> candidates = new ArrayList<Candidate>();

            {
                int score = 10000;
                for (Class<?> currentClass : currentClasses) {
                    for (Field field : OpenBean.fields(currentClass)) {
                        if (field.getName().toLowerCase().startsWith(patternLC)) {
                            usedFields.add(field);
                            candidates.add(new Candidate(new Replacement(field), score--));
                        }
                    }

                    for (MethodDesc method : OpenBean.methods(currentClass)) {
                        if (method.getName().toLowerCase().startsWith(patternLC)) {
                            usedMethods.add(method.getMethod());
                            candidates.add(new Candidate(new Replacement(method), score--));
                        }
                    }
                }
            }

            Collections.sort(candidates, new Comparator<Candidate>() {
                @Override
                public int compare(Candidate o1, Candidate o2) {
                    int d1 = StringUtils.getLevenshteinDistance(o1.r.name.toLowerCase(), patternLC);
                    int d2 = StringUtils.getLevenshteinDistance(o2.r.name.toLowerCase(), patternLC, d1 + 1);
                    return Integer.compare(d1, d2);
                }
            });

            for (int i = 0; i < candidates.size(); i++) {
                Candidate candidate = candidates.get(i);
                candidate.score = 10000 - i;
            }

            char[] chars = pattern.toCharArray();
            for (Class<?> currentClass : currentClasses) {
                for (Field field : OpenBean.fields(currentClass)) {
                    if (usedFields.contains(field))
                        continue;

                    int r = 0;

                    for (char aChar : chars) {
                        r += frequency(field.getName(), aChar);
                    }

                    if (r > 0)
                        candidates.add(new Candidate(new Replacement(field), r));

                }

                for (MethodDesc method : OpenBean.methods(currentClass)) {
                    if (usedMethods.contains(method.getMethod()))
                        continue;

                    int r = 0;

                    for (char aChar : chars) {
                        r += frequency(method.getName(), aChar);
                    }

                    if (r > 0)
                        candidates.add(new Candidate(new Replacement(method), r));
                }
            }

            Collections.sort(candidates);

            replacements.addAll(candidates);
        }

        return replacements;
    }

    private int frequency(String name, char aChar) {
        int r = 0;
        for (int i = 0; i < name.length(); i++) {
            if (name.charAt(i) == aChar)
                r++;
        }

        return r;
    }

    static List<Token> tokenize(String script, int start, int end) {
        Token currentToken;

        int nonSpaceStart = firstNonSpace(script, start, 1);

        List<Token> tokens = Lists.newArrayList(currentToken = new Token(nonSpaceStart));

        char matchingQuote = 0;

        int openedBrackets = 0;
        int closedBrackets = 0;

        for (int pos = nonSpaceStart; pos < end; pos++) {
            char ch = script.charAt(pos);

            //check string mode, copied
            if (ch == '\"' || ch == '\'') {
                matchingQuote = closeOpenQuotes(matchingQuote, ch);

                continue;
            }

            if (matchingQuote != 0)
                continue;

            if (ch == '(' && !currentToken.method) {
                currentToken.method = true;
                int nonSpace = firstNonSpace(script, pos - 1, -1) + 1;
                currentToken.name = script.substring(currentToken.start, nonSpace);
            }

            final boolean isBracket;

            switch (ch) {
            case ')':
                closedBrackets++;
                isBracket = true;
                break;

            case '(':
                openedBrackets++;
                isBracket = true;
                break;
            default:
                isBracket = false;
            }

            if (openedBrackets > closedBrackets) {
                continue;
            }

            if (openedBrackets > 0 && openedBrackets == closedBrackets) {
                currentToken.end = pos + 1;
                openedBrackets = 0;
                closedBrackets = 0;
            }

            if (isBracket)
                continue;

            if (ch != '.' && !Character.isJavaIdentifierPart(ch) && !currentToken.method) {
                //spaces after identifier, in method there can be anything
                currentToken.end = pos;
                currentToken.name = currentToken.text(script);
                continue;
            }

            boolean atEnd = pos >= end - 1;
            boolean isDot = ch == '.';

            if (isDot || atEnd) {
                if (currentToken.end == -1) { //end is not -1 for a method
                    if (atEnd) {
                        if (isDot) {
                            currentToken.end = pos;
                        } else {
                            currentToken.end = end;
                        }
                    } else {
                        currentToken.end = firstNonSpace(script, pos - 1, -1) + 1;
                    }

                    currentToken.name = currentToken.text(script);
                }

                int nonSpace = firstNonSpace(script, pos + 1, 1);

                pos = nonSpace;

                atEnd = pos >= end - 1;

                if (!atEnd) {
                    tokens.add(currentToken = new Token(nonSpace));
                }

                continue;
            }

        }
        return tokens;
    }

    private void groovyParsingSample(String script, int[] range) throws Exception {
        SourceBuffer sourceBuffer = new SourceBuffer();
        String source = script.substring(range[0], range[1]);
        UnicodeEscapingReader unicodeReader = new UnicodeEscapingReader(new StringReader(source), sourceBuffer);
        GroovyLexer lexer = new GroovyLexer(unicodeReader);
        unicodeReader.setLexer(lexer);

        //        parseFile("mem", lexer, sourceBuffer, source);
    }

    /**
     * Returning a range because rightBorder(+startPos+) can move left 1 char when placed at the space.
     * todo check if it replaces ok
     * See unit test to check how it works.
     *
     * @param startPos == script.length if at the end of the script.
     */
    static int[] scanForStart(String script, int startPos, final int inc) {
        int l = script.length();

        char matchingQuote = 0;

        int openedBrackets = 0;
        int closedBrackets = 0;

        int pos;

        startPos--; // ' ' or '(' are not our characters

        for (pos = startPos; pos >= 0 && pos < l; pos += inc) {

            char ch = script.charAt(pos);

            //check string mode, copied
            if (ch == '\"' || ch == '\'') {
                matchingQuote = closeOpenQuotes(matchingQuote, ch);

                continue;
            }

            if (matchingQuote != 0)
                continue;

            //normal mode
            final boolean isBracket;

            // foo2.recursiveFoo(xx()) - here the invariant is:  closed>=opened
            // outerMethod|( foo2.recursiveFoo(xx()) here it breaks
            switch (ch) {
            case ')':
                closedBrackets++;
                isBracket = true;
                break;

            case '(':
                openedBrackets++;
                isBracket = true;
                break;
            default:
                isBracket = false;
            }

            // this case: outerMethod|( foo2.recursiveFoo(xx()) here it breaks
            if (openedBrackets > closedBrackets) {
                return new int[] { firstNonSpace(script, pos + 1, 1), startPos };
            }

            if (openedBrackets < closedBrackets) {
                continue;
            }

            if (isBracket)
                continue;

            if (Character.isJavaIdentifierPart(ch) || ch == '.')
                continue;

            break;
        }

        pos -= inc;
        //        if(pos < 0) pos = 0;
        //        if(pos >= l) pos = l-1;

        return new int[] { pos, startPos };
    }

    private static char closeOpenQuotes(char matchingQuote, char ch) {
        if (matchingQuote == ch) {
            matchingQuote = 0;
        } else if (matchingQuote == 0) {
            matchingQuote = ch;
        } else {
            //just skip
        }
        return matchingQuote;
    }

    private static int firstNonSpace(String script, int pos, int inc) {
        for (; pos < script.length() && pos >= 0 && isWhitespace(script.charAt(pos)); pos += inc) {
        }

        return pos;
    }

    // Here's where we do the real work...
    //needs groovy, not groovy-all
    /*public static void parseFile(String f, GroovyLexer l, SourceBuffer sourceBuffer, String source)
    throws Exception {
    try {
        // Create a parser that reads from the scanner
        final GroovyRecognizer parser = GroovyRecognizer.make(l);
        parser.setSourceBuffer(sourceBuffer);
        parser.setFilename(f);
        
        if (false) {
            GroovyLexer lexer = parser.getLexer();
            lexer.setWhitespaceIncluded(true);
            while (true) {
                antlr.Token t = lexer.nextToken();
                System.out.println(t);
                if (t == null || t.getType() == antlr.Token.EOF_TYPE) break;
            }
            return;
        }
        
        // start parsing at the compilationUnit rule
        parser.compilationUnit();
        
        AST ast = parser.getAST();
        System.out.println("parseFile " + f + " => " + ast);
        
        ASTFactory factory = new ASTFactory();
        AST r = factory.create(0, "AST ROOT");
        
        r.setFirstChild(ast);
        
        GroovySourceAST groovySourceAST = new GroovySourceAST();
        groovySourceAST.initialize(ast);
        
        Visitor visitor = new VisitorAdapter() {
            int level = -1;
        
            @Override
            public void visitDefault(GroovySourceAST t, int visit) {
                if (visit == OPENING_VISIT) {
                    level++;
                    System.out.print(StringUtils.repeat(' ', level * 2) + "opening ");
                }
        
                if (visit == CLOSING_VISIT) {
                    System.out.print(StringUtils.repeat(' ', level * 2) + "closing ");
                    level--;
                }
                System.out.print(parser.getTokenName(t.getType()) + " ");
        
                System.out.println(t.toString());
    //                    System.out.println(t.getSnippet());
            }
        };
        AntlrASTProcessor treewalker = new SourceCodeTraversal(visitor);
        
        treewalker.process(ast);
        
        System.out.println(ast);
        
    } catch (Exception e) {
        System.err.println("parser exception: " + e);
        e.printStackTrace();   // so we can get stack trace
    }
    }*/
}