github.com/emcfarlane/larking@v0.0.0-20220605172417-1704b45ee6c3/starlib/completions.go (about)

     1  // Copyright 2022 Edward McFarlane. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package starlib
     6  
     7  import (
     8  	"sort"
     9  	"strconv"
    10  	"strings"
    11  	"unicode/utf8"
    12  
    13  	"go.starlark.net/starlark"
    14  )
    15  
    16  // finxPrefix assumes sorted arrays of keys
    17  func findPrefix(line string, depth int, pfx string, keyss ...[]string) (c []string) {
    18  	pfx = strings.TrimSpace(pfx) // ignore any whitespacing
    19  	for _, keys := range keyss {
    20  		i := sort.SearchStrings(keys, pfx)
    21  		j := i
    22  		for ; j < len(keys); j++ {
    23  			if !strings.HasPrefix(keys[j], pfx) {
    24  				break
    25  			}
    26  		}
    27  		c = append(c, keys[i:j]...)
    28  	}
    29  	if len(keyss) > 1 {
    30  		sort.Strings(c)
    31  	}
    32  
    33  	// Add line start
    34  	for i := range c {
    35  		c[i] = line[:depth] + c[i]
    36  	}
    37  	return c
    38  }
    39  
    40  // An Args is a starlark Callable with arguments.
    41  type Args interface {
    42  	starlark.Callable
    43  	ArgNames() []string
    44  }
    45  
    46  // Completer is an experimental autocompletion for starlark lines.
    47  // TODO: drop and switch to a proper language server.
    48  type Completer struct {
    49  	starlark.StringDict
    50  }
    51  
    52  type typ int
    53  
    54  const (
    55  	unknown typ = iota - 1
    56  	root        //
    57  	dot         // .
    58  	brack       // []
    59  	paren       // ()
    60  	brace       // {}
    61  )
    62  
    63  func (t typ) String() string {
    64  	switch t {
    65  	case dot:
    66  		return "."
    67  	case brack:
    68  		return "["
    69  	case paren:
    70  		return "("
    71  	case brace:
    72  		return "{"
    73  	default:
    74  		return "?"
    75  	}
    76  }
    77  
    78  func enclosed(line string) (typ, int) {
    79  	k := len(line)
    80  	var parens, bracks, braces int
    81  	for size := 0; k > 0; k -= size {
    82  		var r rune
    83  		r, size = utf8.DecodeLastRuneInString(line[:k])
    84  		switch r {
    85  		case '(':
    86  			parens += 1
    87  		case ')':
    88  			parens -= 1
    89  		case '[':
    90  			bracks += 1
    91  		case ']':
    92  			bracks -= 1
    93  		case '{':
    94  			braces += 1
    95  		case '}':
    96  			braces -= 1
    97  		}
    98  		if parens > 0 {
    99  			return paren, k - size
   100  		}
   101  		if bracks > 0 {
   102  			return brack, k - size
   103  		}
   104  		if braces > 0 {
   105  			return brace, k - size
   106  		}
   107  	}
   108  	return unknown, -1
   109  }
   110  
   111  // Complete tries to resolve a starlark line variable to global named values.
   112  // TODO: use a proper parser to resolve values.
   113  func (c Completer) Complete(line string) (values []string) {
   114  	if strings.Count(line, " ") == len(line) {
   115  		// tab complete indent
   116  		return []string{strings.Repeat(" ", (len(line)/4)*4+4)}
   117  	}
   118  
   119  	type x struct {
   120  		typ   typ
   121  		value string
   122  		depth int
   123  	}
   124  
   125  	var xs []x
   126  
   127  	i := len(line)
   128  	j := i
   129  
   130  Loop:
   131  	for size := 0; i > 0; i -= size {
   132  		var r rune
   133  		switch r, size = utf8.DecodeLastRuneInString(line[:i]); r {
   134  		case '.': // attr
   135  			xs = append(xs, x{dot, line[i:j], i})
   136  		case '[': // index
   137  			xs = append(xs, x{brack, line[i:j], i})
   138  		case '(': // functions
   139  			xs = append(xs, x{paren, line[i:j], i})
   140  		case ' ', ',':
   141  			typ, k := enclosed(line[:i-size])
   142  
   143  			// Use ArgNames as possible completion
   144  			if typ == paren {
   145  				xs = append(xs, x{typ, line[i:j], i})
   146  				i, j = k, k
   147  				continue // loop
   148  			}
   149  
   150  			break Loop
   151  		case ';', '=', '{', '}':
   152  			break Loop // EOF
   153  		default:
   154  			continue // capture
   155  		}
   156  		j = i - size
   157  	}
   158  	xs = append(xs, x{root, line[i:j], i})
   159  
   160  	var cursor starlark.Value
   161  	for i := len(xs) - 1; i >= 0; i-- {
   162  		x := xs[i]
   163  
   164  		switch x.typ {
   165  		case root:
   166  			if i == 0 {
   167  				keys := [][]string{c.Keys(), starlark.Universe.Keys()}
   168  				return findPrefix(line, x.depth, x.value, keys...)
   169  			}
   170  
   171  			if g := c.StringDict[x.value]; g != nil {
   172  				cursor = g
   173  			} else if u := starlark.Universe[x.value]; u != nil {
   174  				cursor = u
   175  			}
   176  		case dot:
   177  			v, ok := cursor.(starlark.HasAttrs)
   178  			if !ok {
   179  				return
   180  			}
   181  
   182  			if i == 0 {
   183  				return findPrefix(line, x.depth, x.value, v.AttrNames())
   184  			}
   185  
   186  			p, err := v.Attr(x.value)
   187  			if p == nil || err != nil {
   188  				return
   189  			}
   190  			cursor = p
   191  		case brack:
   192  			if i != 0 {
   193  				// TODO: resolve arg? fmt.Printf("TODO: resolve arg %s\n", x.value)
   194  				return
   195  			}
   196  
   197  			if strings.HasPrefix(x.value, "\"") {
   198  				v, ok := cursor.(starlark.IterableMapping)
   199  				if !ok {
   200  					return
   201  				}
   202  
   203  				iter := v.Iterate()
   204  				var keys []string
   205  				var p starlark.Value
   206  				for iter.Next(&p) {
   207  					s, ok := starlark.AsString(p)
   208  					if !ok {
   209  						continue // skip
   210  					}
   211  					keys = append(keys, strconv.Quote(s)+"]")
   212  				}
   213  				return findPrefix(line, x.depth, x.value, keys)
   214  			}
   215  			keys := [][]string{c.Keys(), starlark.Universe.Keys()}
   216  			return findPrefix(line, x.depth, x.value, keys...)
   217  
   218  		case paren:
   219  			if i != 0 {
   220  				return // Functions aren't evalutated
   221  			}
   222  
   223  			keys := [][]string{c.Keys(), starlark.Universe.Keys()}
   224  			v, ok := cursor.(Args)
   225  			if ok {
   226  				args := v.ArgNames()
   227  				for i := range args {
   228  					args[i] = args[i] + " = "
   229  				}
   230  				keys = append(keys, args)
   231  			}
   232  
   233  			return findPrefix(line, x.depth, x.value, keys...)
   234  		default:
   235  			return
   236  		}
   237  	}
   238  	return
   239  }