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  }