1package nbview23import (4 "context"5 "slices"6 "strconv"7 "strings"89 "src.rest/cat/012/internal/notebook"10 "src.rest/cat/012/internal/nushell"11 "src.rest/cat/012/internal/ui/theme"12)1314// What nu is asked about a cell, and how its answers map back to the15// cell's source.1617// prepared is the text nu is asked about: a prelude declaring the18// notebook's variables, then the source masked, starting at byte at.19// Masking keeps every byte of the source in its place, so what nu says20// of the text is where it says it in the source.21type prepared struct {22 text string23 at int24 heads [][2]int // the bytes of the source that are its `name =`s25}2627// prepare masks src and declares what it reads: `sales = ls` is asked28// about as ` ls`, $sheet.A1:C9 as $__s1_______, and the prelude29// declares $__s1, $selection, $sheet, names and the names the cell's30// lines assign for the lines after them, each `let x: any = []` so nu31// takes it for whatever it holds.32func prepare(src string, names []string) prepared {33 b := []byte(src)34 parsed := notebook.Parse(src)35 heads := parsed.Heads()36 for _, h := range heads {37 for i := h[0]; i < h[1]; i++ {38 if b[i] != '\n' {39 b[i] = ' '40 }41 }42 }43 names = slices.Clone(names)44 for k, st := range parsed.Stmts {45 if st.Name != "" && k < len(parsed.Stmts)-1 { // assigned for the lines after46 names = append(names, st.Name)47 }48 }49 var pre strings.Builder50 declared := map[string]bool{}51 declare := func(name, value string) {52 if !declared[name] {53 declared[name] = true54 pre.WriteString("let " + name + ": any = " + value + "\n")55 }56 }57 for i, s := range parsed.Ranges {58 v := "__s" + strconv.Itoa(i+1)59 if len(v)+1 > s[1]-s[0] {60 continue61 }62 v += strings.Repeat("_", s[1]-s[0]-len(v)-1)63 copy(b[s[0]:s[1]], "$"+v)64 declare(v, "[]")65 }66 declare(notebook.Selection, "[]")67 declare("sheet", "{}")68 for _, nm := range names {69 if notebook.ValidName(nm) == nil {70 declare(nm, "[]")71 }72 }73 return prepared{text: pre.String() + string(b), at: pre.Len(), heads: heads}74}7576// inHead reports whether src[from:to] is part of an assignment's head.77func (p prepared) inHead(from, to int) bool {78 return slices.ContainsFunc(p.heads, func(h [2]int) bool { return from < h[1] && to > h[0] })79}8081// spans are src's syntax roles from nu's shapes of p's text: the82// tokenizer's for the names the cell assigns and its comments, which nu83// doesn't report, and a number's unit (the kb of 1kb) as part of the84// number.85func (p prepared) spans(src string, shapes []nushell.Shape) []Span {86 var out []Span87 for _, s := range (Tokens{}).Highlight(context.Background(), src) {88 if p.inHead(s.From, s.To) || s.Kind == theme.SyntaxComment {89 out = append(out, s)90 }91 }92 for i, s := range shapes {93 from, to := s.From-p.at, s.To-p.at94 if from < 0 || to > len(src) || from >= to || p.inHead(from, to) {95 continue96 }97 kind, ok := shapeKind(s.Shape, src[from:to])98 if s.Shape == "shape_string" && i > 0 && shapes[i-1].To == s.From && isNumberShape(shapes[i-1].Shape) {99 kind, ok = theme.SyntaxNumber, true100 }101 if ok {102 out = append(out, Span{From: from, To: to, Kind: kind})103 }104 }105 return out106}107108// shapeRoles are the syntax roles of nu's shapes; the rest (flags,109// brackets, garbage, which the checker underlines) show plain.110var shapeRoles = map[string]theme.Syntax{111 "shape_internalcall": theme.SyntaxCommand, "shape_external": theme.SyntaxCommand, "shape_external_resolved": theme.SyntaxCommand,112 "shape_keyword": theme.SyntaxKeyword, "shape_bool": theme.SyntaxKeyword, "shape_nothing": theme.SyntaxKeyword,113 "shape_string": theme.SyntaxString, "shape_string_interpolation": theme.SyntaxString, "shape_raw_string": theme.SyntaxString,114 "shape_glob_interpolation": theme.SyntaxString, "shape_globpattern": theme.SyntaxString, "shape_filepath": theme.SyntaxString,115 "shape_directory": theme.SyntaxString, "shape_externalarg": theme.SyntaxString,116 "shape_variable": theme.SyntaxVariable, "shape_vardecl": theme.SyntaxVariable,117 "shape_int": theme.SyntaxNumber, "shape_float": theme.SyntaxNumber, "shape_filesize": theme.SyntaxNumber,118 "shape_duration": theme.SyntaxNumber, "shape_datetime": theme.SyntaxNumber, "shape_binary": theme.SyntaxNumber,119 "shape_operator": theme.SyntaxOperator, "shape_pipe": theme.SyntaxOperator, "shape_redirection": theme.SyntaxOperator,120 "shape_range": theme.SyntaxOperator,121}122123// shapeKind is the role of text nu shaped so: let, if and def are124// keywords to 012, as the tokenizer has them, though nu calls them.125func shapeKind(shape, text string) (theme.Syntax, bool) {126 k, ok := shapeRoles[shape]127 if k == theme.SyntaxCommand && slices.Contains(keywords, text) {128 k = theme.SyntaxKeyword129 }130 return k, ok131}132133func isNumberShape(shape string) bool { return shape == "shape_int" || shape == "shape_float" }134135// replaced is where completion w starts replacing src before offset:136// the longest stretch ending there, from a word's start or after a . or137// /, that w starts with (ignoring case), since nu doesn't say. When none138// is, the word at the caret.139func replaced(src string, offset int, w string) int {140 start := offset141 for start > 0 && !strings.ContainsRune("|;({[\n", rune(src[start-1])) {142 start--143 }144 lw := strings.ToLower(w)145 for i := start; i < offset; i++ {146 if (i == start || strings.ContainsRune(" \t./", rune(src[i-1]))) && strings.HasPrefix(lw, strings.ToLower(src[i:offset])) {147 return i148 }149 }150 if w != "" && strings.ContainsRune("'\"`", rune(w[0])) { // a quoted path: from its quote151 if q := strings.LastIndexAny(src[start:offset], "'\"`"); q >= 0 {152 return start + q153 }154 }155 i := offset156 for i > start && !isSpace(src[i-1]) {157 i--158 }159 return i160}