src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/diag/context.go (about) 1 package diag 2 3 import ( 4 "fmt" 5 "strings" 6 ) 7 8 // Context stores information derived from a range in some text. It is used for 9 // errors that point to a part of the source code, including parse errors, 10 // compilation errors and a single traceback entry in an exception. 11 // 12 // Context values should only be constructed using [NewContext]. 13 type Context struct { 14 Name string 15 Ranging 16 // 1-based line and column numbers of the start position. 17 StartLine, StartCol int 18 // 1-based line and column numbers of the end position, inclusive. Note that 19 // if the range is zero-width, EndCol will be StartCol - 1. 20 EndLine, EndCol int 21 // The relevant text, text before its the first line and the text after its 22 // last line. 23 Body, Head, Tail string 24 } 25 26 // NewContext creates a new Context. 27 func NewContext(name, source string, r Ranger) *Context { 28 rg := r.Range() 29 d := getContextDetails(source, rg) 30 return &Context{name, rg, 31 d.startLine, d.startCol, d.endLine, d.endCol, d.body, d.head, d.tail} 32 } 33 34 // Show shows the context. 35 // 36 // If the body has only one line, it returns one line like: 37 // 38 // foo.elv:12:7-11: lorem ipsum 39 // 40 // If the body has multiple lines, it shows the body in an indented block: 41 // 42 // foo.elv:12:1-13:5 43 // lorem 44 // ipsum 45 // 46 // The body is underlined. 47 func (c *Context) Show(indent string) string { 48 rangeDesc := c.describeRange() 49 if c.StartLine == c.EndLine { 50 // Body has only one line, show it on the same line: 51 // 52 return fmt.Sprintf("%s: %s", 53 rangeDesc, showContextText(indent, c.Head, c.Body, c.Tail)) 54 } 55 indent += " " 56 return fmt.Sprintf("%s:\n%s%s", 57 rangeDesc, indent, showContextText(indent, c.Head, c.Body, c.Tail)) 58 } 59 60 func (c *Context) describeRange() string { 61 if c.StartLine == c.EndLine { 62 if c.EndCol < c.StartCol { 63 // Since EndCol is inclusive, zero-width ranges result in EndCol = 64 // StartCol - 1. 65 return fmt.Sprintf("%s:%d:%d", c.Name, c.StartLine, c.StartCol) 66 } 67 return fmt.Sprintf("%s:%d:%d-%d", 68 c.Name, c.StartLine, c.StartCol, c.EndCol) 69 } 70 return fmt.Sprintf("%s:%d:%d-%d:%d", 71 c.Name, c.StartLine, c.StartCol, c.EndLine, c.EndCol) 72 } 73 74 // Variables controlling the style used in [*Context.Show]. Can be overridden in 75 // tests. 76 var ( 77 ContextBodyStartMarker = "\033[1;4m" 78 ContextBodyEndMarker = "\033[m" 79 ) 80 81 func showContextText(indent, head, body, tail string) string { 82 var sb strings.Builder 83 sb.WriteString(head) 84 85 for i, line := range strings.Split(body, "\n") { 86 if i > 0 { 87 sb.WriteByte('\n') 88 sb.WriteString(indent) 89 } 90 sb.WriteString(ContextBodyStartMarker) 91 sb.WriteString(line) 92 sb.WriteString(ContextBodyEndMarker) 93 } 94 95 sb.WriteString(tail) 96 return sb.String() 97 } 98 99 // Information about the lines that contain the culprit. 100 type contextDetails struct { 101 startLine, startCol int 102 endLine, endCol int 103 body, head, tail string 104 } 105 106 func getContextDetails(source string, r Ranging) contextDetails { 107 before := source[:r.From] 108 body := source[r.From:r.To] 109 after := source[r.To:] 110 111 head := lastLine(before) 112 113 // If the body ends with a newline, stripe it, and leave the tail empty. 114 // Otherwise, don't process the body and calculate the tail. 115 var tail string 116 if strings.HasSuffix(body, "\n") { 117 body = body[:len(body)-1] 118 } else { 119 tail = firstLine(after) 120 } 121 122 startLine := strings.Count(before, "\n") + 1 123 startCol := 1 + len(head) 124 endLine := startLine + strings.Count(body, "\n") 125 var endCol int 126 if startLine == endLine { 127 endCol = startCol + len(body) - 1 128 } else { 129 endCol = len(lastLine(body)) 130 } 131 132 return contextDetails{startLine, startCol, endLine, endCol, body, head, tail} 133 } 134 135 func firstLine(s string) string { 136 i := strings.IndexByte(s, '\n') 137 if i == -1 { 138 return s 139 } 140 return s[:i] 141 } 142 143 func lastLine(s string) string { 144 // When s does not contain '\n', LastIndexByte returns -1, which happens to 145 // be what we want. 146 return s[strings.LastIndexByte(s, '\n')+1:] 147 }