1package notebook23import (4 "slices"5 "strings"6)78// How 012 reads a code cell's source, as nu does: its comments and9// strings apart from its code, and its statements. A statement is a10// line, or a part of one between `;`, at the top level (outside11// brackets); a pipeline goes on over the next line when that line12// starts with `|` (comment lines between allowed, a blank line not), or13// when its own line ends in `|` or in an assignment's `=`. A statement14// `name = pipeline` assigns name, as nu's `let name = pipeline` does.1516// Source is a code cell's source as 012 reads it.17type Source struct {18 Text string19 // Stmts are its statements, in order.20 Stmts []Stmt21 // Comments are its comments' bytes, # to the end of the line, and22 // Strings its strings', quotes included.23 Comments, Strings [][2]int24 // Ranges are where it reads ranges of sheets, each $sheet.A1:C925 // whole.26 Ranges [][2]int27 // code is Text with its comments blanked, and view is code with its28 // strings blanked too, but for the code interpolated into them:29 // where names are read.30 code, view []byte31}3233// Stmt is a statement of a source, by byte offsets into it.34type Stmt struct {35 // From and To are its bytes, without the spaces and comments around.36 From, To int37 // Body is where what it runs starts: after `name =`, or From.38 Body int39 // Name is what `name = pipeline` assigns, or "".40 Name string41 // Local is the variable `let name = ...` or `mut name = ...` binds,42 // which only the cell's later statements read, or "".43 Local string44}4546// Parse reads src.47func Parse(src string) Source {48 s := &scanner{src: src, view: []byte(src)}49 s.code(0, 0)50 code := []byte(src)51 for _, c := range s.comments {52 blank(code, c[0], c[1])53 }54 return Source{Text: src, Stmts: statements(src, s.view), Comments: s.comments, Strings: s.strs, Ranges: s.ranges, code: code, view: s.view}55}5657// Name is what the source's last statement assigns: the cell's output's58// name, or "".59func (s Source) Name() string {60 if len(s.Stmts) == 0 {61 return ""62 }63 return s.Stmts[len(s.Stmts)-1].Name64}6566// Assigned are the names the statements assign, in the order they67// first do.68func (s Source) Assigned() []string {69 var out []string70 for _, st := range s.Stmts {71 if st.Name != "" && !slices.Contains(out, st.Name) {72 out = append(out, st.Name)73 }74 }75 return out76}7778// Refs are the names the source reads as $name from outside it, in the79// order it first names them: not a name an earlier statement assigns or80// binds with let, nor nushell's variables and the notebook's own81// ($selection, $sheet), nor one in a comment or a string.82func (s Source) Refs() []string {83 var out []string84 v := s.view85 for i := 0; i < len(v); i++ {86 if v[i] != '$' {87 continue88 }89 j := i + 190 for j < len(v) && isWord(v[j]) {91 j++92 }93 name := string(v[i+1 : j])94 if name != "" && ValidName(name) == nil && !slices.Contains(out, name) && !s.local(name, i) {95 out = append(out, name)96 }97 i = j - 198 }99 return out100}101102// local reports whether name, read at offset at, is a variable of the103// cell's own: a statement before assigns or binds it.104func (s Source) local(name string, at int) bool {105 return slices.ContainsFunc(s.Stmts, func(st Stmt) bool {106 return st.To <= at && (st.Name == name || st.Local == name)107 })108}109110// ReadsSelection reports whether the source reads $selection.111func (s Source) ReadsSelection() bool {112 v, want := string(s.view), "$"+Selection113 for i := 0; ; {114 k := strings.Index(v[i:], want)115 if k < 0 {116 return false117 }118 end := i + k + len(want)119 if end == len(v) || !isWord(v[end]) {120 return true121 }122 i = end123 }124}125126// Heads are the bytes of each assignment's `name =`, up to what it runs.127func (s Source) Heads() [][2]int {128 var out [][2]int129 for _, st := range s.Stmts {130 if st.Name != "" {131 out = append(out, [2]int{st.From, st.Body})132 }133 }134 return out135}136137// scanner tells a source's comments and strings from its code.138type scanner struct {139 src string140 view []byte141 comments, strs, ranges [][2]int142}143144// code reads code from i up to the bracket close that ends it (0 for145// the end of the source), returning where it stopped.146func (s *scanner) code(i int, close byte) int {147 for i < len(s.src) {148 c := s.src[i]149 switch {150 case close != 0 && c == close:151 return i152 case c == '(' || c == '[' || c == '{':153 i = s.code(i+1, closer(c))154 if i < len(s.src) {155 i++156 }157 case c == '#' && s.tokenStart(i, close != 0):158 i = s.comment(i)159 case c == '"' || c == '\'' || c == '`':160 i = s.str(i, i+1, c)161 case c == '$':162 i = s.dollar(i)163 case c == 'r' && s.tokenStart(i, close != 0) && rawHashes(s.src[i+1:]) > 0:164 i = s.raw(i)165 default:166 i++167 }168 }169 return i170}171172func closer(c byte) byte {173 switch c {174 case '(':175 return ')'176 case '[':177 return ']'178 }179 return '}'180}181182// tokenStart reports whether a token may start at i: after a space, a183// pipe, a ; or an opening bracket, and inside brackets after a , or a184// : too, where nu reads a # as a comment.185func (s *scanner) tokenStart(i int, inside bool) bool {186 if i == 0 {187 return true188 }189 p := s.src[i-1]190 return strings.IndexByte(" \t\r\n|;([{", p) >= 0 || inside && (p == ',' || p == ':')191}192193func (s *scanner) comment(i int) int {194 j := i195 for j < len(s.src) && s.src[j] != '\n' {196 j++197 }198 s.comments = append(s.comments, [2]int{i, j})199 blank(s.view, i, j)200 return j201}202203// str reads a quoted string opened at start, its text from i: a double204// quoted one has escapes, a single quoted or backquoted one none.205func (s *scanner) str(start, i int, q byte) int {206 for i < len(s.src) && s.src[i] != q {207 if q == '"' && s.src[i] == '\\' {208 i++209 }210 i++211 }212 i = min(i+1, len(s.src))213 s.strs = append(s.strs, [2]int{start, i})214 blank(s.view, start, i)215 return i216}217218// dollar reads what starts with $ at i: an interpolated string, a range219// of a sheet, or a variable.220func (s *scanner) dollar(i int) int {221 rest := s.src[i+1:]222 switch {223 case strings.HasPrefix(rest, `"`) || strings.HasPrefix(rest, "'"):224 return s.interpolated(i)225 case strings.HasPrefix(s.src[i:], sheetVar) && (i == 0 || !isWord(s.src[i-1])):226 from := i + len(sheetVar)227 if n := refLen(s.src[from:]); n > 0 {228 s.ranges = append(s.ranges, [2]int{i, from + n})229 blank(s.view, from, from+n) // its quotes and brackets aren't code230 return from + n231 }232 }233 return i + 1234}235236// interpolated reads $"..." or $'...' at i: text, blanked, and the code237// in its parentheses, read as code.238func (s *scanner) interpolated(i int) int {239 q := s.src[i+1]240 j := i + 2241 blank(s.view, i, j)242 for j < len(s.src) && s.src[j] != q {243 switch {244 case q == '"' && s.src[j] == '\\':245 blank(s.view, j, min(j+2, len(s.src)))246 j += 2247 case s.src[j] == '(':248 j = s.code(j+1, ')')249 if j < len(s.src) {250 j++251 }252 default:253 blank(s.view, j, j+1)254 j++255 }256 }257 j = min(j+1, len(s.src))258 blank(s.view, j-1, j)259 s.strs = append(s.strs, [2]int{i, j})260 return j261}262263// rawHashes is how many # open a raw string at the start of s, r#'...'#264// without its r, or 0.265func rawHashes(s string) int {266 n := 0267 for n < len(s) && s[n] == '#' {268 n++269 }270 if n == 0 || n == len(s) || s[n] != '\'' {271 return 0272 }273 return n274}275276// raw reads a raw string at i, to its ' and as many # as opened it.277func (s *scanner) raw(i int) int {278 n := rawHashes(s.src[i+1:])279 end := "'" + strings.Repeat("#", n)280 body := i + 2 + n281 j := len(s.src)282 if k := strings.Index(s.src[body:], end); k >= 0 {283 j = body + k + len(end)284 }285 s.strs = append(s.strs, [2]int{i, j})286 blank(s.view, i, j)287 return j288}289290// blank makes b[from:to] spaces, but for its line breaks.291func blank(b []byte, from, to int) {292 for i := from; i < to && i < len(b); i++ {293 if b[i] != '\n' {294 b[i] = ' '295 }296 }297}