From 2684ed25614b0471e7beaeab4d6f0d3c1d1340d0 Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Wed, 18 Aug 2021 21:04:16 +0200 Subject: [PATCH] cli: Prepare completion for better variables support --- pkg/interp/completion.go | 90 ++++++++++++++++++++++++++++++----- pkg/interp/completion_test.go | 38 +++++++-------- pkg/interp/interp.jq | 60 ++++++++++++++--------- 3 files changed, 134 insertions(+), 54 deletions(-) diff --git a/pkg/interp/completion.go b/pkg/interp/completion.go index 9c128da6..b54a1747 100644 --- a/pkg/interp/completion.go +++ b/pkg/interp/completion.go @@ -15,11 +15,12 @@ const ( CompletionTypeFunc CompletionType = "function" CompletionTypeVar CompletionType = "variable" CompletionTypeNone CompletionType = "none" + CompletionTypeError CompletionType = "error" ) func BuildCompletionQuery(src string) (*gojq.Query, CompletionType, string) { if src == "" { - return nil, CompletionTypeNone, "" + return nil, CompletionTypeError, "" } // HACK: if ending with "." or "$" append a test index that we remove later @@ -32,7 +33,7 @@ func BuildCompletionQuery(src string) (*gojq.Query, CompletionType, string) { q, err := gojq.Parse(src + probePrefix) if err != nil { - return nil, CompletionTypeNone, "" + return nil, CompletionTypeError, "" } cq, ct, prefix := transformToCompletionQuery(q) @@ -40,7 +41,37 @@ func BuildCompletionQuery(src string) (*gojq.Query, CompletionType, string) { prefix = strings.TrimSuffix(prefix, probePrefix) } - return cq, ct, prefix + if ct == CompletionTypeNone { + return cq, ct, "" + } + + // [.[] | cq | add] + return &gojq.Query{ + Left: &gojq.Query{ + Term: &gojq.Term{ + Type: gojq.TermTypeArray, + Array: &gojq.Array{ + Query: &gojq.Query{ + Left: &gojq.Query{ + Term: &gojq.Term{ + Type: gojq.TermTypeIdentity, + SuffixList: []*gojq.Suffix{{Iter: true}}, + }, + }, + Op: gojq.OpPipe, + Right: cq, + }, + }, + }, + }, + Op: gojq.OpPipe, + Right: &gojq.Query{ + Term: &gojq.Term{ + Type: gojq.TermTypeFunc, + Func: &gojq.Func{Name: "add"}, + }, + }, + }, ct, prefix } // find the right most term that is completeable @@ -56,6 +87,23 @@ func transformToCompletionQuery(q *gojq.Query) (*gojq.Query, CompletionType, str return q, ct, prefix } + keysFuncName := func(name string) string { + if strings.HasPrefix(name, "_") { + return "_extkeys" + } + return "keys" + } + + optFunc := func(name string) *gojq.Query { + return &gojq.Query{ + Term: &gojq.Term{ + Type: gojq.TermTypeFunc, + Func: &gojq.Func{Name: name}, + SuffixList: []*gojq.Suffix{{Optional: true}}, + }, + } + } + // ... as ... if q.Term.SuffixList != nil { last := q.Term.SuffixList[len(q.Term.SuffixList)-1] @@ -70,40 +118,56 @@ func transformToCompletionQuery(q *gojq.Query) (*gojq.Query, CompletionType, str if last.Index != nil && last.Index.Name != "" { prefix := last.Index.Name last.Index = nil - return q, CompletionTypeIndex, prefix + return &gojq.Query{ + Left: q, + Op: gojq.OpPipe, + Right: optFunc(keysFuncName(prefix)), + }, CompletionTypeIndex, prefix } } switch q.Term.Type { //nolint:exhaustive case gojq.TermTypeIdentity: - return q, CompletionTypeIndex, "" + return &gojq.Query{ + Left: q, + Op: gojq.OpPipe, + Right: optFunc(keysFuncName("")), + }, CompletionTypeIndex, "" case gojq.TermTypeIndex: if len(q.Term.SuffixList) == 0 { if q.Term.Index.Start == nil { - return &gojq.Query{Term: &gojq.Term{Type: gojq.TermTypeIdentity}}, CompletionTypeIndex, q.Term.Index.Name + return &gojq.Query{ + Left: &gojq.Query{Term: &gojq.Term{Type: gojq.TermTypeIdentity}}, + Op: gojq.OpPipe, + Right: optFunc(keysFuncName(q.Term.Index.Name)), + }, CompletionTypeIndex, q.Term.Index.Name } - return nil, CompletionTypeNone, "" + return q, CompletionTypeNone, "" } last := q.Term.SuffixList[len(q.Term.SuffixList)-1] if last.Index != nil && last.Index.Start == nil { q.Term.SuffixList = q.Term.SuffixList[0 : len(q.Term.SuffixList)-1] - return q, CompletionTypeIndex, last.Index.Name + return &gojq.Query{ + Left: q, + Op: gojq.OpPipe, + Right: optFunc(keysFuncName(last.Index.Name)), + }, CompletionTypeIndex, last.Index.Name } - return nil, CompletionTypeNone, "" + return q, CompletionTypeNone, "" case gojq.TermTypeFunc: if len(q.Term.SuffixList) == 0 { if strings.HasPrefix(q.Term.Func.Name, "$") { - return &gojq.Query{Term: &gojq.Term{Type: gojq.TermTypeIdentity}}, CompletionTypeVar, q.Term.Func.Name + return optFunc("scope"), CompletionTypeVar, q.Term.Func.Name } else { - return &gojq.Query{Term: &gojq.Term{Type: gojq.TermTypeIdentity}}, CompletionTypeFunc, q.Term.Func.Name + return optFunc("scope"), CompletionTypeFunc, q.Term.Func.Name } } - return nil, CompletionTypeNone, "" + return q, CompletionTypeNone, "" default: - return nil, CompletionTypeNone, "" + return q, CompletionTypeNone, "" } } diff --git a/pkg/interp/completion_test.go b/pkg/interp/completion_test.go index e80ed0a9..632b090a 100644 --- a/pkg/interp/completion_test.go +++ b/pkg/interp/completion_test.go @@ -13,25 +13,25 @@ func TestBuildCompletionQuery(t *testing.T) { expectedType interp.CompletionType expectedPrefix string }{ - {"", "", interp.CompletionTypeNone, ""}, - {`.`, `.`, interp.CompletionTypeIndex, ``}, - {`.`, `.`, interp.CompletionTypeIndex, ``}, - {`.a`, `.`, interp.CompletionTypeIndex, `a`}, - {`.a.`, `.a`, interp.CompletionTypeIndex, ``}, - {`.a.b`, `.a`, interp.CompletionTypeIndex, `b`}, - {`.a.b.`, `.a.b`, interp.CompletionTypeIndex, ``}, - {` .a.b`, `.a`, interp.CompletionTypeIndex, `b`}, - {`.a | .b`, `.a | .`, interp.CompletionTypeIndex, `b`}, - {`.a | .b.c`, `.a | .b`, interp.CompletionTypeIndex, `c`}, - {`.a[]`, ``, interp.CompletionTypeNone, ``}, - {`.a[].b`, `.a[]`, interp.CompletionTypeIndex, `b`}, - {`.a[].b.c`, `.a[].b`, interp.CompletionTypeIndex, `c`}, - {`.a["b"]`, ``, interp.CompletionTypeNone, ``}, - {`.a["b"].c`, `.a["b"]`, interp.CompletionTypeIndex, `c`}, - {`.a[1:2]`, ``, interp.CompletionTypeNone, ``}, - {`.a[1:2].c`, `.a[1:2]`, interp.CompletionTypeIndex, `c`}, - {`a`, `.`, interp.CompletionTypeFunc, `a`}, - {`a | b`, `a | .`, interp.CompletionTypeFunc, `b`}, + {"", "", interp.CompletionTypeError, ""}, + {`.`, `[.[] | . | keys?] | add`, interp.CompletionTypeIndex, ``}, + {`.`, `[.[] | . | keys?] | add`, interp.CompletionTypeIndex, ``}, + {`.a`, `[.[] | . | keys?] | add`, interp.CompletionTypeIndex, `a`}, + {`.a.`, `[.[] | .a | keys?] | add`, interp.CompletionTypeIndex, ``}, + {`.a.b`, `[.[] | .a | keys?] | add`, interp.CompletionTypeIndex, `b`}, + {`.a.b.`, `[.[] | .a.b | keys?] | add`, interp.CompletionTypeIndex, ``}, + {`.a.b`, `[.[] | .a | keys?] | add`, interp.CompletionTypeIndex, `b`}, + {`.a | .b`, `[.[] | .a | . | keys?] | add`, interp.CompletionTypeIndex, `b`}, + {`.a | .b.c`, `[.[] | .a | .b | keys?] | add`, interp.CompletionTypeIndex, `c`}, + {`.a[]`, `.a[]`, interp.CompletionTypeNone, ``}, + {`.a[].b`, `[.[] | .a[] | keys?] | add`, interp.CompletionTypeIndex, `b`}, + {`.a[].b.c`, `[.[] | .a[].b | keys?] | add`, interp.CompletionTypeIndex, `c`}, + {`.a["b"]`, `.a["b"]`, interp.CompletionTypeNone, ``}, + {`.a["b"].c`, `[.[] | .a["b"] | keys?] | add`, interp.CompletionTypeIndex, `c`}, + {`.a[1:2]`, `.a[1:2]`, interp.CompletionTypeNone, ``}, + {`.a[1:2].c`, `[.[] | .a[1:2] | keys?] | add`, interp.CompletionTypeIndex, `c`}, + {`a`, `[.[] | scope?] | add`, interp.CompletionTypeFunc, `a`}, + {`a | b`, `[.[] | a | scope?] | add`, interp.CompletionTypeFunc, `b`}, } for _, tC := range testCases { t.Run(tC.input, func(t *testing.T) { diff --git a/pkg/interp/interp.jq b/pkg/interp/interp.jq index efc49ef4..1a137cf3 100644 --- a/pkg/interp/interp.jq +++ b/pkg/interp/interp.jq @@ -35,43 +35,59 @@ def _exit_code_compile_error: 3; def _exit_code_input_decode_error: 4; def _exit_code_expr_error: 5; +# TODO: refactor this # TODO: completionMode # TODO: return escaped identifier, not sure current readline implementation supports # completions that needs to change previous input, ex: .a\t -> ."a \" b" etc -def _complete($e; $pos): +def _complete($e; $cursor_pos): def _is_internal: startswith("_") or startswith("$_"); + def _query_index_or_key($q): + ( ([.[] | eval($q) | type]) as $n + | if ($n | all(. == "object")) then "." + elif ($n | all(. == "array")) then "[]" + else null + end + ); # only complete if at end of there is a whitespace for now - if ($e[$pos] | . == "" or . == " ") then - ( ( $e[0:$pos] | _complete_query) as {$type, $query, $prefix} + if ($e[$cursor_pos] | . == "" or . == " ") then + ( . as $c + | ( $e[0:$cursor_pos] | _complete_query) as {$type, $query, $prefix} | { prefix: $prefix, names: ( - ( if $type == "function" or $type == "variable" then - [.[] | eval($query) | scope] | add - elif $type == "index" then - [.[] | eval($query) | keys?, _extkeys?] | add - else - [] - end - | ($prefix | _is_internal) as $prefix_is_internal - | map( - select( - strings and - (_is_ident or $type == "variable") and - ((_is_internal | not) or $prefix_is_internal or $type == "index") and - startswith($prefix) - ) + if $type == "none" then + ( $c + | _query_index_or_key($query) + | if . then [.] else [] end ) - | unique - | sort - ) + else + ( $c + | eval($query) + | ($prefix | _is_internal) as $prefix_is_internal + | map( + select( + strings and + (_is_ident or $type == "variable") and + ((_is_internal | not) or $prefix_is_internal or $type == "index") and + startswith($prefix) + ) + ) + | unique + | sort + | if length == 1 and .[0] == $prefix then + ( $c + | _query_index_or_key($e) + | if . then [$prefix+.] else [$prefix] end + ) + end + ) + end ) } ) else {prefix: "", names: []} end; - # for convenience when testing def _complete($e): _complete($e; $e | length); def _obj_to_csv_kv: