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  }