diff --git a/_site/ucl/repl/index.html b/_site/ucl/repl/index.html new file mode 100644 index 0000000..a1529ea --- /dev/null +++ b/_site/ucl/repl/index.html @@ -0,0 +1,8 @@ + + +
+ + + + + \ No newline at end of file diff --git a/cmd/playwasm/jsiter.go b/cmd/playwasm/jsiter.go index dca6b12..1e62e6d 100644 --- a/cmd/playwasm/jsiter.go +++ b/cmd/playwasm/jsiter.go @@ -8,6 +8,7 @@ import ( "github.com/alecthomas/participle/v2" "strings" "syscall/js" + "ucl.lmika.dev/repl" "ucl.lmika.dev/ucl" ) @@ -20,7 +21,7 @@ func invokeUCLCallback(name string, args ...any) { func initJS(ctx context.Context) { uclObj := make(map[string]any) - inst := ucl.New(ucl.WithOut(ucl.LineHandler(func(line string) { + replInst := repl.New(ucl.WithOut(ucl.LineHandler(func(line string) { invokeUCLCallback("onOutLine", line) }))) @@ -36,7 +37,7 @@ func initJS(ctx context.Context) { } wantContinue := args[1].Bool() - if err := ucl.EvalAndDisplay(ctx, inst, cmdLine); err != nil { + if err := replInst.EvalAndDisplay(ctx, cmdLine); err != nil { var p participle.Error if errors.As(err, &p) && wantContinue { invokeUCLCallback("onContinue") @@ -50,21 +51,3 @@ func initJS(ctx context.Context) { }) js.Global().Set("ucl", uclObj) } - -// -//type uclOut struct { -// lineBuffer *bytes.Buffer -// writeLine func(line string) -//} -// -//func (uo *uclOut) Write(p []byte) (n int, err error) { -// for _, b := range p { -// if b == '\n' { -// uo.writeLine(uo.lineBuffer.String()) -// uo.lineBuffer.Reset() -// } else { -// uo.lineBuffer.WriteByte(b) -// } -// } -// return len(p), nil -//} diff --git a/repl/docs.go b/repl/docs.go index 40e3485..6b9eee4 100644 --- a/repl/docs.go +++ b/repl/docs.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "github.com/lmika/gopkgs/fp/maps" - "os" "slices" "sort" "strings" @@ -31,77 +30,100 @@ func (d Doc) config(cmdName string, r *REPL) { } func (r *REPL) helpBuiltin(ctx context.Context, args ucl.CallArgs) (any, error) { - switch { - case args.NArgs() == 0: - names := maps.Keys(r.commandDocs) - sort.Strings(names) + if args.NArgs() == 0 { + return NoResults{}, r.listHelpTopics(maps.Keys(r.commandDocs)) + } - tabWriter := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) + var cmdName string + if err := args.Bind(&cmdName); err != nil { + return nil, err + } - for _, name := range names { - cdoc := r.commandDocs[name] - if cdoc.Brief != "" { - fmt.Fprintf(tabWriter, "%v\t %v\n", name, r.commandDocs[name].Brief) - } else { - fmt.Fprintf(tabWriter, "%v\n", name) - } + found := make([]string, 0) + for name := range r.commandDocs { + if name == cmdName { + return NoResults{}, r.showHelpTopic(name) } - tabWriter.Flush() - default: - var cmdName string - if err := args.Bind(&cmdName); err != nil { - return nil, err - } - - docs, ok := r.commandDocs[cmdName] - if !ok { - return nil, errors.New("no help docs found for command") - } - - fmt.Printf("%v\n", cmdName) - fmt.Printf(" %v\n", docs.Brief) - - if docs.Usage != "" { - fmt.Println("\nUsage:") - fmt.Printf(" %v %v\n", cmdName, docs.Usage) - } - - if len(docs.Args) > 0 { - fmt.Println("\nArguments:") - - docArgs := slices.Clone(docs.Args) - sort.Slice(docArgs, func(i, j int) bool { - return strings.TrimPrefix(docs.Args[i].Name, "-") < strings.TrimPrefix(docs.Args[j].Name, "-") - }) - - tw := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) - for _, arg := range docArgs { - fmt.Fprintf(tw, " %v\t %v\n", arg.Name, arg.Brief) - } - tw.Flush() - } - - if docs.Detailed != "" { - fmt.Println("\nDetails:") - - lines := strings.Split(docs.Detailed, "\n") - - trimLeft := 0 - if len(lines) == 0 { - return nil, nil - } else if len(strings.TrimSpace(lines[0])) == 0 && len(lines) > 1 { - // indicates that the next line should indicate the indentation - trimLeft = len(lines[1]) - len(strings.TrimLeftFunc(lines[1], unicode.IsSpace)) - lines = lines[1:] - } - - for _, line := range lines { - fmt.Printf(" %v\n", trimSpaceLeftUpto(line, trimLeft)) - } + if strings.Contains(name, cmdName) { + found = append(found, name) } } - return NoResults{}, nil + + if len(found) == 0 { + return nil, errors.New("no help found for topic") + } + + return NoResults{}, r.listHelpTopics(found) +} + +func (r *REPL) listHelpTopics(topics []string) error { + sort.Strings(topics) + + tabWriter := tabwriter.NewWriter(r.inst.Out(), 0, 0, 1, ' ', 0) + + for _, name := range topics { + cdoc := r.commandDocs[name] + if cdoc.Brief != "" { + fmt.Fprintf(tabWriter, "%v\t %v\n", name, r.commandDocs[name].Brief) + } else { + fmt.Fprintf(tabWriter, "%v\n", name) + } + } + + tabWriter.Flush() + return nil +} + +func (r *REPL) showHelpTopic(topic string) error { + + docs, ok := r.commandDocs[topic] + if !ok { + return errors.New("no help docs found for command") + } + + fmt.Fprintf(r.inst.Out(), "%v\n", topic) + fmt.Fprintf(r.inst.Out(), " %v\n", docs.Brief) + + if docs.Usage != "" { + fmt.Fprintln(r.inst.Out(), "\nUsage:") + fmt.Fprintf(r.inst.Out(), " %v %v\n", topic, docs.Usage) + } + + if len(docs.Args) > 0 { + fmt.Fprintln(r.inst.Out(), "\nArguments:") + + docArgs := slices.Clone(docs.Args) + sort.Slice(docArgs, func(i, j int) bool { + return strings.TrimPrefix(docs.Args[i].Name, "-") < strings.TrimPrefix(docs.Args[j].Name, "-") + }) + + tw := tabwriter.NewWriter(r.inst.Out(), 0, 0, 1, ' ', 0) + for _, arg := range docArgs { + fmt.Fprintf(tw, " %v\t %v\n", arg.Name, arg.Brief) + } + tw.Flush() + } + + if docs.Detailed != "" { + fmt.Fprintln(r.inst.Out(), "\nDetails:") + + lines := strings.Split(docs.Detailed, "\n") + + trimLeft := 0 + if len(lines) == 0 { + return nil + } else if len(strings.TrimSpace(lines[0])) == 0 && len(lines) > 1 { + // indicates that the next line should indicate the indentation + trimLeft = len(lines[1]) - len(strings.TrimLeftFunc(lines[1], unicode.IsSpace)) + lines = lines[1:] + } + + for _, line := range lines { + fmt.Fprintf(r.inst.Out(), " %v\n", trimSpaceLeftUpto(line, trimLeft)) + } + } + return nil } func trimSpaceLeftUpto(s string, n int) string { diff --git a/repl/evaldisplay.go b/repl/evaldisplay.go index d062778..f09254c 100644 --- a/repl/evaldisplay.go +++ b/repl/evaldisplay.go @@ -18,7 +18,7 @@ func (r *REPL) EvalAndDisplay(ctx context.Context, expr string) error { return err } - return r.displayResult(ctx, os.Stdout, res, false) + return r.displayResult(ctx, r.inst.Out(), res, false) } func (r *REPL) echoPrinter(ctx context.Context, w io.Writer, args []any) (err error) { @@ -89,23 +89,25 @@ func (r *REPL) displayResult(ctx context.Context, w io.Writer, res any, concise fmt.Fprintf(w, "]") } else { // In the off-chance that this is actually a slice of printables - vt := reflect.SliceOf(reflect.TypeOf(v[0])) - if tp, ok := r.typePrinters[vt]; ok { - canDisplay := true + if len(v) > 0 { + vt := reflect.SliceOf(reflect.TypeOf(v[0])) + if tp, ok := r.typePrinters[vt]; ok { + canDisplay := true - typeSlice := reflect.MakeSlice(vt, len(v), len(v)) - for i := 0; i < len(v); i++ { - vv := reflect.ValueOf(v[i]) - if vv.CanConvert(vt.Elem()) { - typeSlice.Index(i).Set(vv) - } else { - canDisplay = false - break + typeSlice := reflect.MakeSlice(vt, len(v), len(v)) + for i := 0; i < len(v); i++ { + vv := reflect.ValueOf(v[i]) + if vv.CanConvert(vt.Elem()) { + typeSlice.Index(i).Set(vv) + } else { + canDisplay = false + break + } } - } - if canDisplay { - return tp(w, typeSlice.Interface(), concise) + if canDisplay { + return tp(w, typeSlice.Interface(), concise) + } } } diff --git a/repl/repl.go b/repl/repl.go index 58997d4..f7ce3cf 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -28,17 +28,17 @@ func New(opts ...ucl.InstOption) *REPL { r.inst = ucl.New(instOpts...) r.SetCommand("help", r.helpBuiltin, Doc{ - Brief: "displays help about a command", - Usage: "[command]", + Brief: "displays help about a command or topic", + Usage: "[topic]", Args: []ArgDoc{ - {Name: "command", Brief: "command to display detailed help for"}, + {Name: "topic", Brief: "topic to display"}, }, Detailed: ` When used without arguments, 'help' will display the list of known commands, along with a brief description on what each one does. - When used with an argument, 'help' will display a more detailed explanation - of what each command does. + If