All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.athaydes.osgiaas.cli.groovy.completer.GroovyCompleter.groovy Maven / Gradle / Ivy

The newest version!
package com.athaydes.osgiaas.cli.groovy.completer

import com.athaydes.osgiaas.api.service.DynamicServiceHelper
import com.athaydes.osgiaas.api.stream.NoOpPrintStream
import com.athaydes.osgiaas.autocomplete.Autocompleter
import com.athaydes.osgiaas.cli.CommandCompleter
import com.athaydes.osgiaas.cli.CommandHelper
import com.athaydes.osgiaas.cli.completer.BaseCompleter
import com.athaydes.osgiaas.cli.completer.CompletionMatcher
import com.athaydes.osgiaas.cli.groovy.command.GroovyCommand
import groovy.transform.CompileStatic
import groovy.transform.Immutable
import org.codehaus.groovy.runtime.DefaultGroovyMethods

import javax.annotation.Nullable
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.util.concurrent.atomic.AtomicReference
import java.util.function.Function

import static CompletionMatcher.nameMatcher

//@CompileStatic
class GroovyCompleter implements CommandCompleter {

    final AtomicReference groovyRef = new AtomicReference<>()

    final BaseCompleter argsMatcher = new BaseCompleter( nameMatcher( 'groovy',
            CompletionMatcher.alternativeMatchers(
                    nameMatcher( GroovyCommand.RESET_CODE_ARG ),
                    nameMatcher( GroovyCommand.SHOW_ARG ),
                    nameMatcher( 'System' ),
                    nameMatcher( 'def' )
            ) ) )

    @Override
    int complete( String buffer, int cursor, List candidates ) {
        if ( !buffer.startsWith( 'groovy ' ) ) {
            // only auto-complete groovy
            return -1
        }

        int result = argsMatcher.complete( buffer, cursor, candidates )

        DynamicServiceHelper.let( groovyRef, { GroovyCommand groovy ->
            int alternativeResult = new DynamicCompleter( groovy.shell.context.variables )
                    .complete( buffer, cursor, candidates )

            if ( alternativeResult < 0 ) {
                alternativeResult = new PropertiesCompleter( groovyRunner: { String script ->
                    def noopStream = new NoOpPrintStream()
                    groovy.run( script, noopStream, noopStream )
                } ).complete( buffer, cursor, candidates )
            }

            if ( alternativeResult >= 0 ) {
                return alternativeResult
            } else {
                return result
            }
        }, { result } )
    }

    void setGroovyCommand( GroovyCommand command ) {
        groovyRef.set command
    }

    void unsetGroovyCommand( GroovyCommand command ) {
        groovyRef.set null
    }
}

@CompileStatic
class DynamicCompleter extends BaseCompleter {

    DynamicCompleter( Map vars ) {
        super( nameMatcher( 'groovy',
                vars.keySet().collect {
                    nameMatcher( it as String )
                } as CompletionMatcher[] ) )
    }
}

@CompileStatic
class PropertiesCompleter implements CommandCompleter {

    private static final Map boxedTypeByPrimitive = [
            ( boolean ): Boolean,
            ( byte )   : Byte,
            ( short )  : Short,
            ( char )   : Character,
            ( int )    : Integer,
            ( float )  : Float,
            ( double ) : Double,
            ( long )   : Long
    ].collectEntries { k, v -> [ ( k ): ResultType.create( v ) ] }.asImmutable() as Map

    final Autocompleter autocompleter = Autocompleter.getDefaultAutocompleter()

    Function groovyRunner

    @Override
    int complete( String buffer, int cursor, List candidates ) {
        String input = buffer.substring( 0, cursor )

        if ( !input.contains( '.' ) ) {
            // no completion
            return -1
        }

        String finalPart = ''

        def breakupOptions = CommandHelper.CommandBreakupOptions.create()
                .includeQuotes( true )
                .quoteCodes( CommandHelper.DOUBLE_QUOTE_CODE, CommandHelper.SINGLE_QUOTE_CODE )

        // break up arguments
        CommandHelper.breakupArguments( input, { finalPart = it; true }, breakupOptions )

        // break up last argument into the actual tokens we're interested (method calls, property access)
        final tokens = [ ] as LinkedList
        CommandHelper.breakupArguments( finalPart, {
            tokens << it.replaceAll( ' ', '' ); true
        }, breakupOptions.separatorCode( ( '.' as char ) as int ) )

        if ( finalPart.endsWith( '.' ) ) {
            tokens << ''
        }

        mergeDigitsIn tokens

        ResultType varType = ResultType.VOID
        final toComplete = tokens.removeLast()
        final tokensIterator = tokens.iterator()

        if ( tokensIterator.hasNext() ) {
            final token = tokensIterator.next()

            // try to evaluate the first token with the existing bindings
            try {
                def result = groovyRunner.apply( token )

                if ( result != null ) {
                    if ( result instanceof Class ) {
                        varType = new ResultType( result, true )
                    } else {
                        varType = new ResultType( result.class, false )
                    }
                }
            } catch ( ignore ) {
            }
        }

        while ( tokensIterator.hasNext() ) {
            def token = tokensIterator.next()

            if ( token.contains( '(' ) && token.endsWith( ')' ) ) { // is method call?
                String methodName = token[ 0..<( token.indexOf( '(' ) ) ]
                def returnType = returnTypeOf( methodName, varType )

                if ( returnType ) {
                    varType = returnType
                    continue // got it
                }
            } else { // is property?
                def field = varType.type.fields.find { it.name == token } as Field

                if ( field ) {
                    varType = ResultType.create( field.type )
                    continue // got it
                } else { // try getter
                    def methodName = 'get' + token.capitalize()
                    def returnType = returnTypeOf( methodName, varType )

                    if ( returnType ) {
                        varType = returnType
                        continue // got it
                    }
                }
            }

            // got here if nothing worked, varType unknown
            varType = ResultType.VOID
        }

        if ( varType ) {
            varType = boxedTypeByPrimitive[ varType.type ] ?: varType

            def fields = varType.type.fields
                    .collect { Field f -> f.name }
                    .sort() as List

            // for class instances, include only static methods, for others, include all
            def methodFilter = varType.isStatic ? this.&isStatic : { true }
            def methods = varType.type.methods.findAll( methodFilter ) as List

            List extraMethods

            if ( varType.isStatic ) {
                extraMethods = Class.getMethods() as List
            } else {
                extraMethods = DefaultGroovyMethods.methods.findAll {
                    it.parameterCount > 0 && it.parameterTypes.first().isAssignableFrom( varType.type )
                } as List
            }

            def completions = autocompleter.completionsFor( toComplete,
                    ( ( methods.sort { it.name } + extraMethods.sort { it.name } )
                            .collectMany( this.&toCompletion ) + fields ).unique() )

            if ( completions ) {
                candidates.addAll( completions )
                return input.findLastIndexOf { it == '.' } + 1
            }
        }

        return -1
    }

    private static List toCompletion( Method method ) {
        // for DefaultGroovyMethods, the first parameter is "self" and should not be counted
        def compensatingFactor = method.declaringClass == DefaultGroovyMethods ? -1 : 0
        def parameterCount = method.parameterCount + compensatingFactor

        if ( method.name.startsWith( 'get' ) &&
                method.name != 'get' &&
                parameterCount == 0 ) {
            return [ uncapitalizeAscii( method.name - 'get' ) ]
        } else if ( parameterCount > 0 ) {
            return [ method.name + '(' ]
        } else {
            return [ method.name + '()' ]
        }
    }

    private static void mergeDigitsIn( LinkedList tokens ) {
        if ( tokens.size() > 2 &&
                tokens[ -2 ].isDouble() &&
                tokens[ -3 ].isInteger() ) {
            String toComplete = tokens.removeLast()
            def last = tokens.removeLast()
            def penultimate = tokens.removeLast()
            tokens << "${penultimate}.${last}".toString() << toComplete
        }
    }

    private static String uncapitalizeAscii( String word ) {
        char[] chars = word.toCharArray()
        chars[ 0 ] += 32
        new String( chars )
    }

    private static boolean isStatic( Method method ) {
        ( method.modifiers & Modifier.STATIC )
    }

    @Nullable
    private static ResultType returnTypeOf( String method, ResultType resultType ) {
        def type = resultType.type
        def keepMethod = { Method m -> resultType.isStatic ? isStatic( m ) : !isStatic( m ) }

        def methods = type.methods.findAll { keepMethod( it ) && it.name == method }

        if ( methods ) {
            // simply choose the first matching method, later we can try to choose the right option
            def methodType = methods.first().returnType
            def isStatic = methodType == Class
            return new ResultType( methodType, isStatic )
        } else {
            return null
        }
    }

}

@Immutable
class ResultType {
    static final ResultType VOID = new ResultType( Void.TYPE, false )
    final Class type
    final boolean isStatic

    static ResultType create( Class type ) {
        new ResultType( type, type == Class )
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy