github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/diag/context.go (about) 1 package diag 2 3 import ( 4 "bytes" 5 "fmt" 6 "strings" 7 8 "github.com/markusbkk/elvish/pkg/wcwidth" 9 ) 10 11 // Context is a range of text in a source code. It is typically used for 12 // errors that can be associated with a part of the source code, like parse 13 // errors and a traceback entry. 14 type Context struct { 15 Name string 16 Source string 17 Ranging 18 19 savedShowInfo *rangeShowInfo 20 } 21 22 // NewContext creates a new Context. 23 func NewContext(name, source string, r Ranger) *Context { 24 return &Context{name, source, r.Range(), nil} 25 } 26 27 // Information about the source range that are needed for showing. 28 type rangeShowInfo struct { 29 // Head is the piece of text immediately before Culprit, extending to, but 30 // not including the closest line boundary. If Culprit already starts after 31 // a line boundary, Head is an empty string. 32 Head string 33 // Culprit is Source[Begin:End], with any trailing newlines stripped. 34 Culprit string 35 // Tail is the piece of text immediately after Culprit, extending to, but 36 // not including the closet line boundary. If Culprit already ends before a 37 // line boundary, Tail is an empty string. 38 Tail string 39 // BeginLine is the (1-based) line number that the first character of Culprit is on. 40 BeginLine int 41 // EndLine is the (1-based) line number that the last character of Culprit is on. 42 EndLine int 43 } 44 45 // Variables controlling the style of the culprit. 46 var ( 47 culpritLineBegin = "\033[1;4m" 48 culpritLineEnd = "\033[m" 49 culpritPlaceHolder = "^" 50 ) 51 52 func (c *Context) RelevantString() string { 53 return c.Source[c.From:c.To] 54 } 55 56 func (c *Context) showInfo() *rangeShowInfo { 57 if c.savedShowInfo != nil { 58 return c.savedShowInfo 59 } 60 61 before := c.Source[:c.From] 62 culprit := c.Source[c.From:c.To] 63 after := c.Source[c.To:] 64 65 head := lastLine(before) 66 beginLine := strings.Count(before, "\n") + 1 67 68 // If the culprit ends with a newline, stripe it. Otherwise, tail is nonempty. 69 var tail string 70 if strings.HasSuffix(culprit, "\n") { 71 culprit = culprit[:len(culprit)-1] 72 } else { 73 tail = firstLine(after) 74 } 75 76 endLine := beginLine + strings.Count(culprit, "\n") 77 78 c.savedShowInfo = &rangeShowInfo{head, culprit, tail, beginLine, endLine} 79 return c.savedShowInfo 80 } 81 82 // Show shows a SourceContext. 83 func (c *Context) Show(sourceIndent string) string { 84 if err := c.checkPosition(); err != nil { 85 return err.Error() 86 } 87 return (c.Name + ", " + c.lineRange() + 88 "\n" + sourceIndent + c.relevantSource(sourceIndent)) 89 } 90 91 // ShowCompact shows a SourceContext, with no line break between the 92 // source position range description and relevant source excerpt. 93 func (c *Context) ShowCompact(sourceIndent string) string { 94 if err := c.checkPosition(); err != nil { 95 return err.Error() 96 } 97 desc := c.Name + ", " + c.lineRange() + " " 98 // Extra indent so that following lines line up with the first line. 99 descIndent := strings.Repeat(" ", wcwidth.Of(desc)) 100 return desc + c.relevantSource(sourceIndent+descIndent) 101 } 102 103 func (c *Context) checkPosition() error { 104 if c.From == -1 { 105 return fmt.Errorf("%s, unknown position", c.Name) 106 } else if c.From < 0 || c.To > len(c.Source) || c.From > c.To { 107 return fmt.Errorf("%s, invalid position %d-%d", c.Name, c.From, c.To) 108 } 109 return nil 110 } 111 112 func (c *Context) lineRange() string { 113 info := c.showInfo() 114 115 if info.BeginLine == info.EndLine { 116 return fmt.Sprintf("line %d:", info.BeginLine) 117 } 118 return fmt.Sprintf("line %d-%d:", info.BeginLine, info.EndLine) 119 } 120 121 func (c *Context) relevantSource(sourceIndent string) string { 122 info := c.showInfo() 123 124 var buf bytes.Buffer 125 buf.WriteString(info.Head) 126 127 culprit := info.Culprit 128 if culprit == "" { 129 culprit = culpritPlaceHolder 130 } 131 132 for i, line := range strings.Split(culprit, "\n") { 133 if i > 0 { 134 buf.WriteByte('\n') 135 buf.WriteString(sourceIndent) 136 } 137 buf.WriteString(culpritLineBegin) 138 buf.WriteString(line) 139 buf.WriteString(culpritLineEnd) 140 } 141 142 buf.WriteString(info.Tail) 143 return buf.String() 144 } 145 146 func firstLine(s string) string { 147 i := strings.IndexByte(s, '\n') 148 if i == -1 { 149 return s 150 } 151 return s[:i] 152 } 153 154 func lastLine(s string) string { 155 // When s does not contain '\n', LastIndexByte returns -1, which happens to 156 // be what we want. 157 return s[strings.LastIndexByte(s, '\n')+1:] 158 }