1package nushell23import (4 "cmp"5 "context"6 "encoding/json"7 "fmt"8 "strconv"9 "strings"10)1112// What nu says of the word at a byte of a script, through13//14// nu --ide-hover 7 file {"hover":"Sort by the given cell path or closure.\n### Usage\n...","span":{"start":5,"end":12}}15//16// A command's hover is its help in Markdown (Help reads it), a17// variable's is its type ("int"), a flag's is just "flag", and a byte18// on no word prints nothing. nu --lsp answers the same through the19// language server's protocol; a process per question, as the other20// questions are, needs no session with it.2122// Hover is nu's text about the word at a byte, and the word's bytes.23type Hover struct {24 From, To int25 Text string26}2728// HoverAt is what nu says of the word at offset, a byte of script;29// false when it's on none.30func HoverAt(ctx context.Context, r Runner, script string, offset int) (Hover, bool, error) {31 out, err := ask(ctx, r, script, "--ide-hover", strconv.Itoa(offset))32 if err != nil {33 return Hover{}, false, err34 }35 return ParseHover(out)36}3738// ParseHover reads what --ide-hover printed.39func ParseHover(out []byte) (Hover, bool, error) {40 if len(out) == 0 {41 return Hover{}, false, nil42 }43 var h struct {44 Hover *string45 Span *span46 }47 if err := json.Unmarshal(out, &h); err != nil || h.Hover == nil || h.Span == nil {48 return Hover{}, false, fmt.Errorf("%w: %s", ErrOld, firstLine(out))49 }50 return Hover{From: h.Span.Start, To: h.Span.End, Text: *h.Hover}, true, nil51}5253// Help is a command's help, as its hover has it.54type Help struct {55 Name string // sort-by, str join56 Desc string // what it does, its paragraphs57 Usage string // sort-by {flags} <...comparator>58 // Flags are its flags but --help; Params its positional59 // parameters, in order.60 Flags []Flag61 Params []Param62 // Types are its input and output types, "table | table".63 Types []string64 Examples []Example65}6667// Flag is a command's flag: -r, --reverse, the type of its value if it68// takes one, and what it does.69type Flag struct {70 Short, Long, Arg, Desc string71}7273// Param is a positional parameter: comparator, its type as nu writes it74// (oneof<cell-path, closure>), and what it is.75type Param struct {76 Name, Type, Desc string77}7879// Example is one of a command's examples: what it does, and the code.80type Example struct {81 Desc, Code string82}8384// ParseHelp reads a command's hover; false when it isn't one (a85// variable's type, a flag's "flag").86func ParseHelp(md string) (Help, bool) {87 desc, rest, ok := strings.Cut(md, "### Usage")88 if !ok {89 return Help{}, false90 }91 h := Help{Desc: strings.TrimSpace(desc)}92 for _, sec := range strings.Split("### Usage"+rest, "\n### ") {93 head, body, _ := strings.Cut(strings.TrimPrefix(sec, "### "), "\n")94 switch strings.TrimSpace(head) {95 case "Usage":96 h.Usage = strings.TrimSpace(strings.Trim(strings.TrimSpace(body), "`"))97 h.Name = commandName(h.Usage)98 case "Flags":99 h.Flags = parseFlags(body)100 case "Parameters":101 h.Params = parseParams(body)102 case "Input/output types":103 h.Types = fenced(body)104 case "Example(s)":105 h.Examples = parseExamples(body)106 }107 }108 return h, h.Name != ""109}110111// commandName is the words of usage before its flags and parameters.112func commandName(usage string) string {113 var words []string114 for w := range strings.FieldsSeq(usage) {115 if strings.ContainsAny(w[:1], "{<(.[") {116 break117 }118 words = append(words, w)119 }120 return strings.Join(words, " ")121}122123// fenced are the lines of body's first code fence, trimmed.124func fenced(body string) []string {125 var out []string126 in := false127 for line := range strings.SplitSeq(body, "\n") {128 t := strings.TrimSpace(line)129 switch {130 case strings.HasPrefix(t, "```") && in:131 return out132 case strings.HasPrefix(t, "```"):133 in = true134 case in && t != "":135 out = append(out, t)136 }137 }138 return out139}140141// ticked splits `-r`, `--reverse` `<int>` - Sort in reverse order.\142// into its backquoted parts and what follows " - ".143func ticked(line string) ([]string, string) {144 line = strings.TrimSuffix(strings.TrimSpace(line), "\\")145 var parts []string146 for strings.HasPrefix(line, "`") {147 end := strings.Index(line[1:], "`")148 if end < 0 {149 break150 }151 parts = append(parts, line[1:end+1])152 line = strings.TrimLeft(line[end+2:], ", ")153 }154 return parts, strings.TrimSpace(strings.TrimPrefix(line, "- "))155}156157func parseFlags(body string) []Flag {158 var out []Flag159 for line := range strings.SplitSeq(body, "\n") {160 parts, desc := ticked(line)161 var f Flag162 for _, p := range parts {163 switch {164 case strings.HasPrefix(p, "--"):165 f.Long = p166 case strings.HasPrefix(p, "-"):167 f.Short = p168 case strings.HasPrefix(p, "<"):169 f.Arg = strings.Trim(p, "<>")170 }171 }172 if (f.Long != "" || f.Short != "") && f.Long != "--help" {173 f.Desc = desc174 out = append(out, f)175 }176 }177 return out178}179180func parseParams(body string) []Param {181 var out []Param182 for line := range strings.SplitSeq(body, "\n") {183 parts, desc := ticked(line)184 if len(parts) == 0 {185 continue186 }187 name, typ, _ := strings.Cut(parts[0], ":")188 out = append(out, Param{Name: strings.TrimSpace(name), Type: strings.TrimSpace(typ), Desc: desc})189 }190 return out191}192193// parseExamples reads the examples: an empty fence, then each194// example's description outside a fence and its code inside one.195func parseExamples(body string) []Example {196 segs := strings.Split(body, "```")197 var out []Example198 for i := 2; i+1 < len(segs); i += 2 {199 desc := strings.TrimSpace(segs[i])200 var code []string201 for line := range strings.SplitSeq(strings.Trim(segs[i+1], "\n"), "\n") {202 code = append(code, strings.TrimPrefix(line, " "))203 }204 c := strings.TrimSpace(strings.Join(code, "\n"))205 if desc != "" || c != "" {206 out = append(out, Example{Desc: desc, Code: c})207 }208 }209 return out210}211212// Summary is the first paragraph of the description.213func (h Help) Summary() string {214 first, _, _ := strings.Cut(h.Desc, "\n\n")215 return strings.Join(strings.Fields(first), " ")216}217218// Signature is the command's usage with its parameters' types and its219// flags by name: sort-by <...comparator: cell-path|closure> --reverse.220func (h Help) Signature() string {221 sig := []string{h.Name}222 rest := strings.TrimSpace(strings.TrimPrefix(h.Usage, h.Name))223 for w := range strings.FieldsSeq(rest) {224 if w == "{flags}" {225 continue226 }227 if p, ok := h.param(w); ok && p.Type != "" {228 w = w[:len(w)-1] + ": " + typeWords(p.Type) + w[len(w)-1:]229 }230 sig = append(sig, w)231 }232 for _, f := range h.Flags {233 name := cmp.Or(f.Long, f.Short)234 if f.Arg != "" {235 name += " <" + f.Arg + ">"236 }237 sig = append(sig, name)238 }239 return strings.Join(sig, " ")240}241242// Brief is the signature where there's no room for it: the usage243// without types, and … for its flags: sort-by <...comparator> ….244func (h Help) Brief() string {245 brief := strings.Join(strings.Fields(strings.Replace(h.Usage, "{flags}", "", 1)), " ")246 if len(h.Flags) > 0 {247 brief += " …"248 }249 return brief250}251252// param is the parameter usage's word w names: <...comparator>,253// <rows?>.254func (h Help) param(w string) (Param, bool) {255 name := strings.Trim(w, "<>()[]?.")256 for _, p := range h.Params {257 if strings.Trim(p.Name, ".?") == name {258 return p, true259 }260 }261 return Param{}, false262}263264// typeWords writes oneof<int, filesize> as int|filesize.265func typeWords(t string) string {266 if in, ok := strings.CutPrefix(t, "oneof<"); ok {267 return strings.ReplaceAll(strings.TrimSuffix(in, ">"), ", ", "|")268 }269 return t270}271272// Flag is the command's flag named w, long (--reverse) or short (-r).273func (h Help) Flag(w string) (Flag, bool) {274 for _, f := range h.Flags {275 if w != "" && (f.Long == w || f.Short == w) {276 return f, true277 }278 }279 return Flag{}, false280}281282// DocsURL is the page of nushell's documentation for command name.283func DocsURL(name string) string {284 return "https://www.nushell.sh/commands/docs/" + strings.ReplaceAll(name, " ", "_") + ".html"285}