Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 31 additions & 34 deletions internal/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ import (
_ "embed"
"io"
"reflect"
"strconv"
"strings"

"github.com/dop251/goja"

"github.com/antonmedv/fx/internal/jsonx"
"github.com/antonmedv/fx/internal/pretty"
)

//go:embed stdlib.js
Expand All @@ -29,46 +27,37 @@ type Parser interface {
Recover() *jsonx.Node
}

type Options struct {
Slurp bool
WithInline bool
WriteOut func(string)
WriteErr func(string)
type Error struct {
error string
}

func Start(parser Parser, args []string, opts Options) int {
if opts.Slurp {
var ok bool
parser, ok = Slurp(parser, opts.WriteErr)
if !ok {
return 1
}
}
func (e *Error) Error() string {
return e.error
}

func Start(parser Parser, args []string, out chan *jsonx.Node, errCh chan error, cancel <-chan struct{}) int {
isPrettyPrintArg := len(args) == 1 && (args[0] == "." || args[0] == "this" || args[0] == "x")

// Fast path.
if isPrettyPrintArg {
for {
select {
case <-cancel:
return 0
default:
}

node, err := parser.Parse()

if err != nil {
if err == io.EOF {
break
}
opts.WriteErr(err.Error())
errCh <- err
return 1
}

if node.Kind == jsonx.String {
unquoted, err := strconv.Unquote(node.Value)
if err != nil {
panic(err)
}
opts.WriteOut(unquoted)
} else {
opts.WriteOut(pretty.Print(node, opts.WithInline))
}
out <- node
}

return 0
Expand All @@ -78,8 +67,8 @@ func Start(parser Parser, args []string, opts Options) int {
if err := validateSyntax(args, i); err != nil {
jsCode := transpile(args[i])
snippet := formatErr(args, i, jsCode)
message := errorToString(err)
opts.WriteErr(snippet + message)
message := gojaErrorToString(err)
errCh <- &Error{snippet + message}
return 1
}
}
Expand All @@ -88,9 +77,11 @@ func Start(parser Parser, args []string, opts Options) int {
code.WriteString(Stdlib)
code.WriteString(JS(args))

vm := NewVM(opts.WriteOut)
vm := NewVM(func(s string) {
out <- &jsonx.Node{Kind: jsonx.Err, Value: s}
})
if _, err := vm.RunString(code.String()); err != nil {
opts.WriteErr(errorToString(err))
errCh <- &Error{gojaErrorToString(err)}
return 1
}

Expand All @@ -101,26 +92,32 @@ func Start(parser Parser, args []string, opts Options) int {
echo := func(output goja.Value) {
rtype := output.ExportType()
if output.StrictEquals(undefined) {
opts.WriteErr("undefined")
errCh <- &Error{"undefined"}
} else if rtype != nil && rtype.Kind() == reflect.String {
opts.WriteOut(output.String())
out <- &jsonx.Node{Kind: jsonx.String, Value: output.String()}
} else {
jsonOut := Stringify(output, vm, 0)
nodeOut, err := jsonx.Parse([]byte(jsonOut))
if err != nil {
panic(err)
}
opts.WriteOut(pretty.Print(nodeOut, opts.WithInline))
out <- nodeOut
}
}

for {
select {
case <-cancel:
return 0
default:
}

node, err := parser.Parse()
if err != nil {
if err == io.EOF {
break
}
opts.WriteErr(err.Error())
errCh <- err
return 1
}

Expand All @@ -130,7 +127,7 @@ func Start(parser Parser, args []string, opts Options) int {
return exitCode
}
if err != nil {
opts.WriteErr(errorToString(err))
errCh <- &Error{gojaErrorToString(err)}
return 1
}

Expand Down
120 changes: 64 additions & 56 deletions internal/engine/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,52 @@ package engine_test

import (
"strings"
"sync"
"testing"

"github.com/stretchr/testify/assert"

"github.com/antonmedv/fx/internal/engine"
"github.com/antonmedv/fx/internal/jsonx"
"github.com/antonmedv/fx/internal/pretty"
)

// runEngine runs the engine with the given parser and args, collecting outputs and errors.
// It returns the exit code, collected outputs, and collected errors.
func runEngine(parser engine.Parser, args []string) (exitCode int, outs []string, errs []string) {
out := make(chan *jsonx.Node)
errCh := make(chan error)
cancel := make(chan struct{})

var wg sync.WaitGroup
wg.Add(2)

go func() {
defer wg.Done()
for node := range out {
if node.Kind == jsonx.String {
outs = append(outs, node.Value)
} else {
outs = append(outs, pretty.Print(node, false))
}
}
}()

go func() {
defer wg.Done()
for err := range errCh {
errs = append(errs, err.Error())
}
}()

exitCode = engine.Start(parser, args, out, errCh, cancel)
close(out)
close(errCh)
wg.Wait()

return exitCode, outs, errs
}

func TestEngine(t *testing.T) {
tests := []struct {
name string
Expand All @@ -22,7 +60,7 @@ func TestEngine(t *testing.T) {
name: "fast path: string as raw",
input: `"Hello, world!"`,
args: []string{"."},
expects: []string{"Hello, world!"},
expects: []string{"\"Hello, world!\""},
errCount: 0,
},
{
Expand All @@ -45,17 +83,7 @@ func TestEngine(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
parser := jsonx.NewJsonParser(strings.NewReader(tc.input), false)

var outs, errs []string
writeOut := func(s string) { outs = append(outs, s) }
writeErr := func(s string) { errs = append(errs, s) }

opts := engine.Options{
Slurp: false,
WithInline: false,
WriteOut: writeOut,
WriteErr: writeErr,
}
exitCode := engine.Start(parser, tc.args, opts)
exitCode, outs, errs := runEngine(parser, tc.args)

assert.Equal(t, 0, exitCode)
assert.Len(t, errs, tc.errCount, "%s: unexpected error count", tc.name)
Expand All @@ -68,17 +96,7 @@ func TestStart_InvalidJSON(t *testing.T) {
input := `{"unclosed": 1`
parser := jsonx.NewJsonParser(strings.NewReader(input), false)

var outs, errs []string
writeOut := func(s string) { outs = append(outs, s) }
writeErr := func(s string) { errs = append(errs, s) }

opts := engine.Options{
Slurp: false,
WithInline: false,
WriteOut: writeOut,
WriteErr: writeErr,
}
exitCode := engine.Start(parser, []string{".unclosed + '!'"}, opts)
exitCode, _, errs := runEngine(parser, []string{".unclosed + '!'"})

assert.Equal(t, 1, exitCode)
assert.Len(t, errs, 1, "Expected one error message")
Expand All @@ -88,17 +106,7 @@ func TestStart_FastPath_InvalidJSON(t *testing.T) {
input := `{"unclosed": 1`
parser := jsonx.NewJsonParser(strings.NewReader(input), false)

var outs, errs []string
writeOut := func(s string) { outs = append(outs, s) }
writeErr := func(s string) { errs = append(errs, s) }

opts := engine.Options{
Slurp: false,
WithInline: false,
WriteOut: writeOut,
WriteErr: writeErr,
}
exitCode := engine.Start(parser, []string{"."}, opts)
exitCode, _, errs := runEngine(parser, []string{"."})

assert.Equal(t, 1, exitCode)
assert.Len(t, errs, 1, "Expected one error message")
Expand All @@ -108,17 +116,7 @@ func TestStart_EscapeSequences(t *testing.T) {
input := `{"emoji": "\ud83d\ude80"}`
parser := jsonx.NewJsonParser(strings.NewReader(input), false)

var outs, errs []string
writeOut := func(s string) { outs = append(outs, s) }
writeErr := func(s string) { errs = append(errs, s) }

opts := engine.Options{
Slurp: false,
WithInline: false,
WriteOut: writeOut,
WriteErr: writeErr,
}
exitCode := engine.Start(parser, []string{".emoji"}, opts)
exitCode, outs, errs := runEngine(parser, []string{".emoji"})

assert.Equal(t, 0, exitCode)
assert.Len(t, errs, 0, "Expected no error messages")
Expand All @@ -129,18 +127,28 @@ func TestStart_EscapeSequences_in_key(t *testing.T) {
input := `{"\ud83d\ude80": "\ud83d\ude80"}`
parser := jsonx.NewJsonParser(strings.NewReader(input), false)

var outs, errs []string
writeOut := func(s string) { outs = append(outs, s) }
writeErr := func(s string) { errs = append(errs, s) }

opts := engine.Options{
Slurp: false,
WithInline: false,
WriteOut: writeOut,
WriteErr: writeErr,
}
exitCode := engine.Start(parser, []string{"x => x"}, opts)
exitCode, _, errs := runEngine(parser, []string{"x => x"})

assert.Equal(t, 0, exitCode)
assert.Len(t, errs, 0, "Expected no error messages")
}

func TestStart_Cancel(t *testing.T) {
// Create a parser that would produce multiple values
input := "1 2 3 4 5"
parser := jsonx.NewJsonParser(strings.NewReader(input), false)

out := make(chan *jsonx.Node, 10)
errCh := make(chan error, 10)
cancel := make(chan struct{})

// Close cancel immediately to test cancellation
close(cancel)

exitCode := engine.Start(parser, []string{"."}, out, errCh, cancel)
close(out)
close(errCh)

// Should return 0 on cancellation
assert.Equal(t, 0, exitCode)
}
7 changes: 3 additions & 4 deletions internal/engine/slurp.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"github.com/antonmedv/fx/internal/jsonx"
)

func Slurp(parser Parser, writeErr func(string)) (Parser, bool) {
func Slurp(parser Parser) (Parser, error) {
arr := &jsonx.Node{
Kind: jsonx.Array,
Value: "[",
Expand All @@ -20,8 +20,7 @@ func Slurp(parser Parser, writeErr func(string)) (Parser, bool) {
if err == io.EOF {
break
}
writeErr(err.Error())
return nil, false
return nil, err
}

node.Parent = arr
Expand All @@ -46,7 +45,7 @@ func Slurp(parser Parser, writeErr func(string)) (Parser, bool) {
}
arr.End = end.Next

return &slurpParser{node: arr}, true
return &slurpParser{node: arr}, nil
}

type slurpParser struct {
Expand Down
2 changes: 1 addition & 1 deletion internal/engine/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func extractErrorMessage(s string) string {
return s
}

func errorToString(err error) string {
func gojaErrorToString(err error) string {
if exception, ok := err.(*goja.Exception); ok {
message := exception.Value().String()
message = extractErrorMessage(message)
Expand Down
8 changes: 6 additions & 2 deletions keymap.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type KeyMap struct {
ShowSelector key.Binding `category:"View"`
GoBack key.Binding `category:"Navigation"`
GoForward key.Binding `category:"Navigation"`
Query key.Binding `category:"Other"`
Help key.Binding `category:"Other"`
CommandLine key.Binding `category:"Other"`
Quit key.Binding `category:"Other"`
Expand Down Expand Up @@ -92,6 +93,10 @@ func init() {
key.WithKeys("up", "k"),
key.WithHelp("", "up"),
),
Query: key.NewBinding(
key.WithKeys("."),
key.WithHelp("", "open query input"),
),
Help: key.NewBinding(
key.WithKeys("?"),
key.WithHelp("", "show help"),
Expand Down Expand Up @@ -193,8 +198,7 @@ var (
yankKey = key.NewBinding(key.WithKeys("k"))
yankPath = key.NewBinding(key.WithKeys("p"))
yankKeyValue = key.NewBinding(key.WithKeys("b"))
arrowUp = key.NewBinding(key.WithKeys("up"))
arrowDown = key.NewBinding(key.WithKeys("down"))
showSizes = key.NewBinding(key.WithKeys("s"))
showLineNumbers = key.NewBinding(key.WithKeys("l"))
ctrlC = key.NewBinding(key.WithKeys("ctrl+c"))
)
Loading