github.com/xiaq/elvish@v0.12.0/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  }