1package notebook23import (4 "slices"5 "strconv"6 "strings"7)89// statements splits a source into its statements, reading view, the10// source with its comments and strings blanked.11func statements(src string, view []byte) []Stmt {12 var out []Stmt13 depth, from := 0, 014 for i := 0; i <= len(view); i++ {15 if i < len(view) {16 switch c := view[i]; {17 case c == '(' || c == '[' || c == '{':18 depth++19 continue20 case c == ')' || c == ']' || c == '}':21 depth = max(depth-1, 0)22 continue23 case c != ';' && c != '\n', depth > 0, c == '\n' && continues(src, view, from, i):24 continue25 }26 }27 if st, ok := statement(view, from, i); ok {28 out = append(out, st)29 }30 from = i + 131 }32 return out33}3435// continues reports whether the statement from from goes on past the36// line break at i: its line ends in | or in an assignment's =, or the37// next line, past comment lines, starts with |.38func continues(src string, view []byte, from, i int) bool {39 before := strings.TrimRight(string(view[from:i]), " \t\r")40 if strings.HasSuffix(before, "|") || strings.HasSuffix(before, "=") {41 return true42 }43 for j := i + 1; j < len(view); {44 end := j45 for end < len(view) && view[end] != '\n' {46 end++47 }48 line := strings.TrimSpace(string(view[j:end]))49 switch {50 case strings.HasPrefix(line, "|"):51 return true52 case line != "" || strings.TrimSpace(src[j:end]) == "":53 return false // code, or a blank line54 }55 j = end + 1 // a comment line56 }57 return false58}5960// statement is the statement in view[from:to], if there's one.61func statement(view []byte, from, to int) (Stmt, bool) {62 for from < to && isSpace(view[from]) {63 from++64 }65 for to > from && isSpace(view[to-1]) {66 to--67 }68 if from == to {69 return Stmt{}, false70 }71 st := Stmt{From: from, To: to, Body: from}72 name, rest := word(view, from, to)73 rest = skipSpaces(view, rest, to, " \t")74 switch {75 case rest < to && view[rest] == '=' && ValidName(name) == nil && !opAt(view, rest+1, to):76 st.Name, st.Body = name, skipSpaces(view, rest+1, to, " \t\r\n")77 case name == "let" || name == "mut" || name == "const":78 st.Local, _ = word(view, skipSpaces(view, rest, to, " \t"), to)79 }80 return st, true81}8283// opAt reports whether an = ending at i is part of an operator: ==, =~.84func opAt(view []byte, i, to int) bool { return i < to && (view[i] == '=' || view[i] == '~') }8586// word is the word of letters, digits and _ at from, and where it ends.87func word(view []byte, from, to int) (string, int) {88 i := from89 for i < to && isWord(view[i]) {90 i++91 }92 return string(view[from:i]), i93}9495func skipSpaces(view []byte, i, to int, spaces string) int {96 for i < to && strings.IndexByte(spaces, view[i]) >= 0 {97 i++98 }99 return i100}101102func isSpace(c byte) bool { return c == ' ' || c == '\t' || c == '\r' || c == '\n' }103104// OutputVar is the name of the variable that holds a cell's output in105// the record a run hands back when it also hands back variables106// (nushell.ExecVars).107const OutputVar = "__out"108109// Command is what nu runs for the source: each statement but the last110// that assigns a name as `let name = ...`, the last without its `name =`111// (its value is the output), the ranges of sheets renamed to variables112// of their own ($__sheet1), and the comments blanked, so what's left113// keeps its place. When statements before the last assign names the114// last doesn't, those are exports, and the command ends in a record of115// the output (as __out) and each of them.116func (s Source) Command() (cmd string, exports []string, ranges []SheetRef) {117 return s.command(true)118}119120// StreamCommand is what nu runs for the source as a stream: Command121// without the record, so the last statement's values stream as they122// come, and the names the statements before it assign stay the run's.123func (s Source) StreamCommand() string {124 cmd, _, _ := s.command(false)125 return cmd126}127128func (s Source) command(record bool) (cmd string, exports []string, ranges []SheetRef) {129 var edits []edit130 for _, r := range s.Ranges {131 ref := s.Text[r[0]+len(sheetVar) : r[1]]132 i := slices.IndexFunc(ranges, func(x SheetRef) bool { return x.Ref == ref })133 if i < 0 {134 ranges = append(ranges, SheetRef{Ref: ref, Var: "__sheet" + strconv.Itoa(len(ranges)+1)})135 i = len(ranges) - 1136 }137 edits = append(edits, edit{r[0], r[1], "$" + ranges[i].Var})138 }139 if len(s.Stmts) == 0 {140 return apply(s.code, edits), nil, ranges141 }142 last := s.Stmts[len(s.Stmts)-1]143 for _, st := range s.Stmts[:len(s.Stmts)-1] {144 if st.Name == "" {145 continue146 }147 edits = append(edits, edit{st.From, st.Body, "let " + st.Name + " = "})148 if st.Name != last.Name && !slices.Contains(exports, st.Name) {149 exports = append(exports, st.Name)150 }151 }152 if last.Name != "" {153 edits = append(edits, edit{last.From, last.Body, ""})154 }155 if !record {156 exports = nil157 }158 if len(exports) > 0 {159 var rec strings.Builder160 rec.WriteString(")")161 for _, e := range exports {162 rec.WriteString(", " + e + ": $" + e)163 }164 rec.WriteString("}")165 edits = append(edits, edit{last.Body, last.Body, "{" + OutputVar + ": ("}, edit{last.To, last.To, rec.String()})166 }167 return apply(s.code, edits), exports, ranges168}169170// edit replaces b[from:to] with text.171type edit struct {172 from, to int173 text string174}175176// apply makes edits to b, which don't overlap.177func apply(b []byte, edits []edit) string {178 slices.SortStableFunc(edits, func(x, y edit) int {179 if x.from != y.from {180 return x.from - y.from181 }182 return x.to - y.to183 })184 var out strings.Builder185 at := 0186 for _, e := range edits {187 out.Write(b[at:e.from])188 out.WriteString(e.text)189 at = e.to190 }191 out.Write(b[at:])192 return out.String()193}