Skip to content

Commit

Permalink
cli: Prepare completion for better variables support
Browse files Browse the repository at this point in the history
  • Loading branch information
wader committed Sep 12, 2021
1 parent e666380 commit 2684ed2
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 53 deletions.
90 changes: 77 additions & 13 deletions pkg/interp/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,15 +33,45 @@ 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)
if probePrefix != "" {
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
Expand All @@ -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]
Expand All @@ -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, ""

}
}
Expand Down
38 changes: 19 additions & 19 deletions pkg/interp/completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
58 changes: 37 additions & 21 deletions pkg/interp/interp.jq
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
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
)
| unique
| sort
)
end
)
}
)
else
{prefix: "", names: []}
end;
# for convenience when testing
def _complete($e): _complete($e; $e | length);

def _obj_to_csv_kv:
Expand Down

0 comments on commit 2684ed2

Please sign in to comment.