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