cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / nushell / ide.go
ide.go 5.8K
1package nushell23import (4 "bufio"5 "bytes"6 "context"7 "encoding/json"8 "errors"9 "fmt"10 "strconv"11 "strings"12)1314// What nu says of a script without running it, through its IDE flags,15// each reading the script from a file:16//17// nu --ide-ast file [{"type":"ast","span":{"start":0,"end":2},"shape":"shape_internalcall","content":"ls"}, ...]18// nu --ide-check 20 file {"type":"diagnostic","severity":"Error","message":"Variable not found.","span":{"start":31,"end":35}}19// one object a line, "hint" lines among them20// nu --ide-complete 7 file {"completions": ["sort", "sort-by"]}21//22// Spans and offsets are bytes of the file. These are the shapes of nu23// 0.116 (testdata/ide holds its answers); an older nu whose answers24// don't read as these is reported as ErrOld, and a nu older than25// MinVersion isn't asked at all.2627// MinVersion is the oldest nu asked about scripts.28const MinVersion = "0.100.0"2930// ErrOld is a nu too old to ask, or one whose answers don't read.31var ErrOld = errors.New("nu is too old to ask about pipelines")3233// Shape is a stretch of a script, by byte offsets, and nu's name for34// what it is: shape_internalcall, shape_string.35type Shape struct {36 From, To int37 Shape string38}3940// Problem is what nu's check found wrong with a stretch of a script.41type Problem struct {42 From, To int43 Severity string // Error or Warning44 Msg string45}4647// maxAnswer is the most of an answer kept: a script's shapes or48// completions are far less.49const maxAnswer = 4 << 205051// ask runs job, asking nu about script, and returns what it printed.52func ask(ctx context.Context, r Runner, script string, flags ...string) ([]byte, error) {53 ctx, cancel := context.WithCancelCause(ctx)54 defer cancel(nil)55 out := &capped{max: maxAnswer, stop: func() { cancel(ErrTooLarge) }}56 err := r.Run(ctx, Job{Command: script, IDE: flags}, script, out)57 switch {58 case ctx.Err() != nil:59 return nil, context.Cause(ctx)60 case err != nil:61 return nil, err62 }63 return bytes.TrimSpace(out.b.Bytes()), nil64}6566// Version is nu's version, checked against MinVersion: ErrOld when it's67// older or doesn't read as one.68func Version(ctx context.Context, r Runner) (string, error) {69 out, err := ask(ctx, r, "", "--version")70 if err != nil {71 return "", err72 }73 v := string(out)74 if !AtLeast(v, MinVersion) {75 return v, fmt.Errorf("%w: %s", ErrOld, v)76 }77 return v, nil78}7980// AtLeast reports whether version v is oldest or later; one that doesn't81// read as major.minor.patch isn't.82func AtLeast(v, oldest string) bool {83 a, okA := semver(v)84 b, okB := semver(oldest)85 if !okA || !okB {86 return false87 }88 for i := range a {89 if a[i] != b[i] {90 return a[i] > b[i]91 }92 }93 return true94}9596func semver(v string) ([3]int, bool) {97 var out [3]int98 v, _, _ = strings.Cut(strings.TrimSpace(v), "-") // 0.117.0-nightly99 parts := strings.Split(v, ".")100 if len(parts) != 3 {101 return out, false102 }103 for i, p := range parts {104 n, err := strconv.Atoi(p)105 if err != nil {106 return out, false107 }108 out[i] = n109 }110 return out, true111}112113// AST is nu's shapes of script's tokens, in order.114func AST(ctx context.Context, r Runner, script string) ([]Shape, error) {115 out, err := ask(ctx, r, script, "--ide-ast")116 if err != nil {117 return nil, err118 }119 return ParseAST(out)120}121122type span struct{ Start, End int }123124// ParseAST reads what --ide-ast printed.125func ParseAST(out []byte) ([]Shape, error) {126 var items []struct {127 Type string128 Span *span129 Shape string130 }131 if err := json.Unmarshal(out, &items); err != nil {132 return nil, fmt.Errorf("%w: %v", ErrOld, err)133 }134 shapes := make([]Shape, 0, len(items))135 for _, it := range items {136 if it.Type != "ast" || it.Span == nil || !strings.HasPrefix(it.Shape, "shape_") {137 continue138 }139 shapes = append(shapes, Shape{From: it.Span.Start, To: it.Span.End, Shape: it.Shape})140 }141 return shapes, nil142}143144// Check is what nu's check finds wrong with script, at most limit145// problems, each once.146func Check(ctx context.Context, r Runner, script string, limit int) ([]Problem, error) {147 out, err := ask(ctx, r, script, "--ide-check", strconv.Itoa(limit))148 if err != nil {149 return nil, err150 }151 return ParseCheck(out)152}153154// ParseCheck reads what --ide-check printed: its diagnostics, each once,155// leaving out its hints (the types it inferred).156func ParseCheck(out []byte) ([]Problem, error) {157 var probs []Problem158 sc := bufio.NewScanner(bytes.NewReader(out))159 sc.Buffer(nil, maxAnswer)160 for sc.Scan() {161 line := bytes.TrimSpace(sc.Bytes())162 if len(line) == 0 {163 continue164 }165 var d struct {166 Type, Severity, Message string167 Span *span168 }169 if err := json.Unmarshal(line, &d); err != nil {170 return nil, fmt.Errorf("%w: %v", ErrOld, err)171 }172 if d.Type != "diagnostic" || d.Span == nil {173 continue174 }175 p := Problem{From: d.Span.Start, To: d.Span.End, Severity: d.Severity, Msg: d.Message}176 if !containsProblem(probs, p) {177 probs = append(probs, p)178 }179 }180 return probs, sc.Err()181}182183func containsProblem(probs []Problem, p Problem) bool {184 for _, q := range probs {185 if q == p {186 return true187 }188 }189 return false190}191192// Complete is what nu would complete at offset, a byte of script, in193// its order. nu says what goes there, not what it replaces.194func Complete(ctx context.Context, r Runner, script string, offset int) ([]string, error) {195 out, err := ask(ctx, r, script, "--ide-complete", strconv.Itoa(offset))196 if err != nil {197 return nil, err198 }199 return ParseComplete(out)200}201202// ParseComplete reads what --ide-complete printed.203func ParseComplete(out []byte) ([]string, error) {204 if len(out) == 0 {205 return nil, nil206 }207 var c struct {208 Completions *[]string209 }210 if err := json.Unmarshal(out, &c); err != nil || c.Completions == nil {211 return nil, fmt.Errorf("%w: %s", ErrOld, firstLine(out))212 }213 return *c.Completions, nil214}215216func firstLine(b []byte) string {217 line, _, _ := bytes.Cut(b, []byte("\n"))218 return string(line)219}