849 lines
20 KiB
Go
849 lines
20 KiB
Go
package logger
|
|
|
|
// Logging is currently designed to look and feel like clang's error format.
|
|
// Errors are streamed asynchronously as they happen, each error contains the
|
|
// contents of the line with the error, and the error count is limited by
|
|
// default.
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
type Log struct {
|
|
AddMsg func(Msg)
|
|
HasErrors func() bool
|
|
Done func() []Msg
|
|
}
|
|
|
|
type LogLevel int8
|
|
|
|
const (
|
|
LevelNone LogLevel = iota
|
|
LevelInfo
|
|
LevelWarning
|
|
LevelError
|
|
LevelSilent
|
|
)
|
|
|
|
type MsgKind uint8
|
|
|
|
const (
|
|
Error MsgKind = iota
|
|
Warning
|
|
Note
|
|
)
|
|
|
|
func (kind MsgKind) String() string {
|
|
switch kind {
|
|
case Error:
|
|
return "error"
|
|
case Warning:
|
|
return "warning"
|
|
case Note:
|
|
return "note"
|
|
default:
|
|
panic("Internal error")
|
|
}
|
|
}
|
|
|
|
type Msg struct {
|
|
Kind MsgKind
|
|
Data MsgData
|
|
Notes []MsgData
|
|
}
|
|
|
|
type MsgData struct {
|
|
Text string
|
|
Location *MsgLocation
|
|
}
|
|
|
|
type MsgLocation struct {
|
|
File string
|
|
Namespace string
|
|
Line int // 1-based
|
|
Column int // 0-based, in bytes
|
|
Length int // in bytes
|
|
LineText string
|
|
}
|
|
|
|
type Loc struct {
|
|
// This is the 0-based index of this location from the start of the file, in bytes
|
|
Start int32
|
|
}
|
|
|
|
type Range struct {
|
|
Loc Loc
|
|
Len int32
|
|
}
|
|
|
|
func (r Range) End() int32 {
|
|
return r.Loc.Start + r.Len
|
|
}
|
|
|
|
// This type is just so we can use Go's native sort function
|
|
type SortableMsgs []Msg
|
|
|
|
func (a SortableMsgs) Len() int { return len(a) }
|
|
func (a SortableMsgs) Swap(i int, j int) { a[i], a[j] = a[j], a[i] }
|
|
|
|
func (a SortableMsgs) Less(i int, j int) bool {
|
|
ai := a[i]
|
|
aj := a[j]
|
|
aiLoc := ai.Data.Location
|
|
ajLoc := aj.Data.Location
|
|
if aiLoc == nil || ajLoc == nil {
|
|
return aiLoc == nil && ajLoc != nil
|
|
}
|
|
if aiLoc.File != ajLoc.File {
|
|
return aiLoc.File < ajLoc.File
|
|
}
|
|
if aiLoc.Line != ajLoc.Line {
|
|
return aiLoc.Line < ajLoc.Line
|
|
}
|
|
if aiLoc.Column != ajLoc.Column {
|
|
return aiLoc.Column < ajLoc.Column
|
|
}
|
|
if ai.Kind != aj.Kind {
|
|
return ai.Kind < aj.Kind
|
|
}
|
|
return ai.Data.Text < aj.Data.Text
|
|
}
|
|
|
|
// This is used to represent both file system paths (Namespace == "file") and
|
|
// abstract module paths (Namespace != "file"). Abstract module paths represent
|
|
// "virtual modules" when used for an input file and "package paths" when used
|
|
// to represent an external module.
|
|
type Path struct {
|
|
Text string
|
|
Namespace string
|
|
}
|
|
|
|
func (a Path) ComesBeforeInSortedOrder(b Path) bool {
|
|
return a.Namespace > b.Namespace || (a.Namespace == b.Namespace && a.Text < b.Text)
|
|
}
|
|
|
|
type Source struct {
|
|
Index uint32
|
|
|
|
// This is used as a unique key to identify this source file. It should never
|
|
// be shown to the user (e.g. never print this to the terminal).
|
|
//
|
|
// If it's marked as an absolute path, it's a platform-dependent path that
|
|
// includes environment-specific things such as Windows backslash path
|
|
// separators and potentially the user's home directory. Only use this for
|
|
// passing to syscalls for reading and writing to the file system. Do not
|
|
// include this in any output data.
|
|
//
|
|
// If it's marked as not an absolute path, it's an opaque string that is used
|
|
// to refer to an automatically-generated module.
|
|
KeyPath Path
|
|
|
|
// This is used for error messages and the metadata JSON file.
|
|
//
|
|
// This is a mostly platform-independent path. It's relative to the current
|
|
// working directory and always uses standard path separators. Use this for
|
|
// referencing a file in all output data. These paths still use the original
|
|
// case of the path so they may still work differently on file systems that
|
|
// are case-insensitive vs. case-sensitive.
|
|
PrettyPath string
|
|
|
|
// An identifier that is mixed in to automatically-generated symbol names to
|
|
// improve readability. For example, if the identifier is "util" then the
|
|
// symbol for an "export default" statement will be called "util_default".
|
|
IdentifierName string
|
|
|
|
Contents string
|
|
}
|
|
|
|
func (s *Source) TextForRange(r Range) string {
|
|
return s.Contents[r.Loc.Start : r.Loc.Start+r.Len]
|
|
}
|
|
|
|
func (s *Source) RangeOfOperatorBefore(loc Loc, op string) Range {
|
|
text := s.Contents[:loc.Start]
|
|
index := strings.LastIndex(text, op)
|
|
if index >= 0 {
|
|
return Range{Loc: Loc{Start: int32(index)}, Len: int32(len(op))}
|
|
}
|
|
return Range{Loc: loc}
|
|
}
|
|
|
|
func (s *Source) RangeOfOperatorAfter(loc Loc, op string) Range {
|
|
text := s.Contents[loc.Start:]
|
|
index := strings.Index(text, op)
|
|
if index >= 0 {
|
|
return Range{Loc: Loc{Start: loc.Start + int32(index)}, Len: int32(len(op))}
|
|
}
|
|
return Range{Loc: loc}
|
|
}
|
|
|
|
func (s *Source) RangeOfString(loc Loc) Range {
|
|
text := s.Contents[loc.Start:]
|
|
if len(text) == 0 {
|
|
return Range{Loc: loc, Len: 0}
|
|
}
|
|
|
|
quote := text[0]
|
|
if quote == '"' || quote == '\'' {
|
|
// Search for the matching quote character
|
|
for i := 1; i < len(text); i++ {
|
|
c := text[i]
|
|
if c == quote {
|
|
return Range{Loc: loc, Len: int32(i + 1)}
|
|
} else if c == '\\' {
|
|
i += 1
|
|
}
|
|
}
|
|
}
|
|
|
|
return Range{Loc: loc, Len: 0}
|
|
}
|
|
|
|
func (s *Source) RangeOfNumber(loc Loc) (r Range) {
|
|
text := s.Contents[loc.Start:]
|
|
r = Range{Loc: loc, Len: 0}
|
|
|
|
if len(text) > 0 {
|
|
if c := text[0]; c >= '0' && c <= '9' {
|
|
r.Len = 1
|
|
for int(r.Len) < len(text) {
|
|
c := text[r.Len]
|
|
if (c < '0' || c > '9') && (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && c != '.' && c != '_' {
|
|
break
|
|
}
|
|
r.Len++
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func plural(prefix string, count int) string {
|
|
if count == 1 {
|
|
return fmt.Sprintf("%d %s", count, prefix)
|
|
}
|
|
return fmt.Sprintf("%d %ss", count, prefix)
|
|
}
|
|
|
|
func errorAndWarningSummary(errors int, warnings int) string {
|
|
switch {
|
|
case errors == 0:
|
|
return plural("warning", warnings)
|
|
case warnings == 0:
|
|
return plural("error", errors)
|
|
default:
|
|
return fmt.Sprintf("%s and %s",
|
|
plural("warning", warnings),
|
|
plural("error", errors))
|
|
}
|
|
}
|
|
|
|
type TerminalInfo struct {
|
|
IsTTY bool
|
|
UseColorEscapes bool
|
|
Width int
|
|
}
|
|
|
|
func NewStderrLog(options StderrOptions) Log {
|
|
var mutex sync.Mutex
|
|
var msgs SortableMsgs
|
|
terminalInfo := GetTerminalInfo(os.Stderr)
|
|
errors := 0
|
|
warnings := 0
|
|
errorLimitWasHit := false
|
|
|
|
switch options.Color {
|
|
case ColorNever:
|
|
terminalInfo.UseColorEscapes = false
|
|
case ColorAlways:
|
|
terminalInfo.UseColorEscapes = SupportsColorEscapes
|
|
}
|
|
|
|
return Log{
|
|
AddMsg: func(msg Msg) {
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
msgs = append(msgs, msg)
|
|
|
|
// Be silent if we're past the limit so we don't flood the terminal
|
|
if errorLimitWasHit {
|
|
return
|
|
}
|
|
|
|
switch msg.Kind {
|
|
case Error:
|
|
errors++
|
|
if options.LogLevel <= LevelError {
|
|
writeStringWithColor(os.Stderr, msg.String(options, terminalInfo))
|
|
}
|
|
case Warning:
|
|
warnings++
|
|
if options.LogLevel <= LevelWarning {
|
|
writeStringWithColor(os.Stderr, msg.String(options, terminalInfo))
|
|
}
|
|
}
|
|
|
|
// Silence further output if we reached the error limit
|
|
if options.ErrorLimit != 0 && errors >= options.ErrorLimit {
|
|
errorLimitWasHit = true
|
|
if options.LogLevel <= LevelError {
|
|
writeStringWithColor(os.Stderr, fmt.Sprintf(
|
|
"%s reached (disable error limit with --error-limit=0)\n", errorAndWarningSummary(errors, warnings)))
|
|
}
|
|
}
|
|
},
|
|
HasErrors: func() bool {
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
return errors > 0
|
|
},
|
|
Done: func() []Msg {
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
|
|
// Print out a summary if the error limit wasn't hit
|
|
if !errorLimitWasHit && options.LogLevel <= LevelInfo && (warnings != 0 || errors != 0) {
|
|
writeStringWithColor(os.Stderr, fmt.Sprintf("%s\n", errorAndWarningSummary(errors, warnings)))
|
|
}
|
|
|
|
sort.Stable(msgs)
|
|
return msgs
|
|
},
|
|
}
|
|
}
|
|
|
|
func PrintErrorToStderr(osArgs []string, text string) {
|
|
PrintMessageToStderr(osArgs, Msg{Kind: Error, Data: MsgData{Text: text}})
|
|
}
|
|
|
|
func StderrOptionsForArgs(osArgs []string) StderrOptions {
|
|
options := StderrOptions{IncludeSource: true}
|
|
|
|
// Implement a mini argument parser so these options always work even if we
|
|
// haven't yet gotten to the general-purpose argument parsing code
|
|
for _, arg := range osArgs {
|
|
switch arg {
|
|
case "--color=false":
|
|
options.Color = ColorNever
|
|
case "--color=true":
|
|
options.Color = ColorAlways
|
|
case "--log-level=info":
|
|
options.LogLevel = LevelInfo
|
|
case "--log-level=warning":
|
|
options.LogLevel = LevelWarning
|
|
case "--log-level=error":
|
|
options.LogLevel = LevelError
|
|
case "--log-level=silent":
|
|
options.LogLevel = LevelSilent
|
|
}
|
|
}
|
|
|
|
return options
|
|
}
|
|
|
|
func PrintMessageToStderr(osArgs []string, msg Msg) {
|
|
log := NewStderrLog(StderrOptionsForArgs(osArgs))
|
|
log.AddMsg(msg)
|
|
log.Done()
|
|
}
|
|
|
|
type Colors struct {
|
|
Default string
|
|
Dim string
|
|
Red string
|
|
Green string
|
|
Underline string
|
|
}
|
|
|
|
func PrintTextToStderr(level LogLevel, osArgs []string, callback func(Colors) string) {
|
|
options := StderrOptionsForArgs(osArgs)
|
|
|
|
// Skip logging these if these logs are disabled
|
|
if options.LogLevel > level {
|
|
return
|
|
}
|
|
|
|
var useColorEscapes bool
|
|
switch options.Color {
|
|
case ColorNever:
|
|
useColorEscapes = false
|
|
case ColorAlways:
|
|
useColorEscapes = SupportsColorEscapes
|
|
case ColorIfTerminal:
|
|
useColorEscapes = GetTerminalInfo(os.Stderr).UseColorEscapes
|
|
}
|
|
|
|
var colors Colors
|
|
if useColorEscapes {
|
|
colors.Default = colorReset
|
|
colors.Dim = colorResetDim
|
|
colors.Red = colorRed
|
|
colors.Green = colorGreen
|
|
colors.Underline = colorResetUnderline
|
|
}
|
|
writeStringWithColor(os.Stderr, callback(colors))
|
|
}
|
|
|
|
func NewDeferLog() Log {
|
|
var msgs SortableMsgs
|
|
var mutex sync.Mutex
|
|
var hasErrors bool
|
|
|
|
return Log{
|
|
AddMsg: func(msg Msg) {
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
if msg.Kind == Error {
|
|
hasErrors = true
|
|
}
|
|
msgs = append(msgs, msg)
|
|
},
|
|
HasErrors: func() bool {
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
return hasErrors
|
|
},
|
|
Done: func() []Msg {
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
sort.Stable(msgs)
|
|
return msgs
|
|
},
|
|
}
|
|
}
|
|
|
|
const colorReset = "\033[0m"
|
|
const colorRed = "\033[31m"
|
|
const colorGreen = "\033[32m"
|
|
const colorMagenta = "\033[35m"
|
|
const colorResetDim = "\033[0;37m"
|
|
const colorBold = "\033[1m"
|
|
const colorResetBold = "\033[0;1m"
|
|
const colorResetUnderline = "\033[0;4m"
|
|
|
|
type StderrColor uint8
|
|
|
|
const (
|
|
ColorIfTerminal StderrColor = iota
|
|
ColorNever
|
|
ColorAlways
|
|
)
|
|
|
|
type StderrOptions struct {
|
|
IncludeSource bool
|
|
ErrorLimit int
|
|
Color StderrColor
|
|
LogLevel LogLevel
|
|
}
|
|
|
|
func (msg Msg) String(options StderrOptions, terminalInfo TerminalInfo) string {
|
|
// Compute the maximum margin
|
|
maxMargin := 0
|
|
if options.IncludeSource {
|
|
if msg.Data.Location != nil {
|
|
maxMargin = len(fmt.Sprintf("%d", msg.Data.Location.Line))
|
|
}
|
|
for _, note := range msg.Notes {
|
|
if note.Location != nil {
|
|
margin := len(fmt.Sprintf("%d", note.Location.Line))
|
|
if margin > maxMargin {
|
|
maxMargin = margin
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Format the messages
|
|
text := msgString(options, terminalInfo, msg.Kind, msg.Data, maxMargin)
|
|
for _, note := range msg.Notes {
|
|
text += msgString(options, terminalInfo, Note, note, maxMargin)
|
|
}
|
|
|
|
// Add extra spacing between messages if source code is present
|
|
if options.IncludeSource {
|
|
text += "\n"
|
|
}
|
|
return text
|
|
}
|
|
|
|
// The number of margin characters in addition to the line number
|
|
const extraMarginChars = 7
|
|
|
|
func marginWithLineText(maxMargin int, line int) string {
|
|
number := fmt.Sprintf("%d", line)
|
|
return fmt.Sprintf(" %s%s │ ", strings.Repeat(" ", maxMargin-len(number)), number)
|
|
}
|
|
|
|
func emptyMarginText(maxMargin int) string {
|
|
return fmt.Sprintf(" %s ╵ ", strings.Repeat(" ", maxMargin))
|
|
}
|
|
|
|
func msgString(options StderrOptions, terminalInfo TerminalInfo, kind MsgKind, data MsgData, maxMargin int) string {
|
|
var kindColor string
|
|
textColor := colorBold
|
|
textResetColor := colorResetBold
|
|
textIndent := ""
|
|
|
|
if options.IncludeSource {
|
|
textIndent = " > "
|
|
}
|
|
|
|
switch kind {
|
|
case Error:
|
|
kindColor = colorRed
|
|
|
|
case Warning:
|
|
kindColor = colorMagenta
|
|
|
|
case Note:
|
|
textColor = colorReset
|
|
kindColor = colorResetBold
|
|
textResetColor = colorReset
|
|
if options.IncludeSource {
|
|
textIndent = strings.Repeat(" ", maxMargin+extraMarginChars-3)
|
|
}
|
|
|
|
default:
|
|
panic("Internal error")
|
|
}
|
|
|
|
if data.Location == nil {
|
|
if terminalInfo.UseColorEscapes {
|
|
return fmt.Sprintf("%s%s%s%s: %s%s%s\n",
|
|
textColor, textIndent, kindColor, kind.String(),
|
|
textResetColor, data.Text,
|
|
colorReset)
|
|
}
|
|
|
|
return fmt.Sprintf("%s%s: %s\n", textIndent, kind.String(), data.Text)
|
|
}
|
|
|
|
if !options.IncludeSource {
|
|
if terminalInfo.UseColorEscapes {
|
|
return fmt.Sprintf("%s%s%s: %s%s: %s%s%s\n",
|
|
textColor, textIndent, data.Location.File,
|
|
kindColor, kind.String(),
|
|
textResetColor, data.Text,
|
|
colorReset)
|
|
}
|
|
|
|
return fmt.Sprintf("%s%s: %s: %s\n",
|
|
textIndent, data.Location.File, kind.String(), data.Text)
|
|
}
|
|
|
|
d := detailStruct(data, terminalInfo, maxMargin)
|
|
|
|
if terminalInfo.UseColorEscapes {
|
|
return fmt.Sprintf("%s%s%s: %s%s: %s%s\n%s%s%s%s%s%s\n%s%s%s%s%s%s\n",
|
|
textColor, textIndent, d.Path,
|
|
kindColor, kind.String(),
|
|
textResetColor, d.Message,
|
|
colorResetDim, d.SourceBefore, colorGreen, d.SourceMarked, colorResetDim, d.SourceAfter,
|
|
d.Indent, colorGreen, d.Marker,
|
|
colorResetDim, d.ContentAfter, colorReset)
|
|
}
|
|
|
|
return fmt.Sprintf("%s%s: %s: %s\n%s%s%s\n%s%s%s\n",
|
|
textIndent, d.Path, kind.String(), d.Message,
|
|
d.SourceBefore, d.SourceMarked, d.SourceAfter,
|
|
d.Indent, d.Marker, d.ContentAfter)
|
|
}
|
|
|
|
type MsgDetail struct {
|
|
Path string
|
|
Line int
|
|
Column int
|
|
Message string
|
|
|
|
SourceBefore string
|
|
SourceMarked string
|
|
SourceAfter string
|
|
|
|
Indent string
|
|
Marker string
|
|
|
|
ContentAfter string
|
|
}
|
|
|
|
func computeLineAndColumn(contents string, offset int) (lineCount int, columnCount int, lineStart int, lineEnd int) {
|
|
var prevCodePoint rune
|
|
if offset > len(contents) {
|
|
offset = len(contents)
|
|
}
|
|
|
|
// Scan up to the offset and count lines
|
|
for i, codePoint := range contents[:offset] {
|
|
switch codePoint {
|
|
case '\n':
|
|
lineStart = i + 1
|
|
if prevCodePoint != '\r' {
|
|
lineCount++
|
|
}
|
|
case '\r':
|
|
lineStart = i + 1
|
|
lineCount++
|
|
case '\u2028', '\u2029':
|
|
lineStart = i + 3 // These take three bytes to encode in UTF-8
|
|
lineCount++
|
|
}
|
|
prevCodePoint = codePoint
|
|
}
|
|
|
|
// Scan to the end of the line (or end of file if this is the last line)
|
|
lineEnd = len(contents)
|
|
loop:
|
|
for i, codePoint := range contents[offset:] {
|
|
switch codePoint {
|
|
case '\r', '\n', '\u2028', '\u2029':
|
|
lineEnd = offset + i
|
|
break loop
|
|
}
|
|
}
|
|
|
|
columnCount = offset - lineStart
|
|
return
|
|
}
|
|
|
|
func LocationOrNil(source *Source, r Range) *MsgLocation {
|
|
if source == nil {
|
|
return nil
|
|
}
|
|
|
|
// Convert the index into a line and column number
|
|
lineCount, columnCount, lineStart, lineEnd := computeLineAndColumn(source.Contents, int(r.Loc.Start))
|
|
|
|
return &MsgLocation{
|
|
File: source.PrettyPath,
|
|
Line: lineCount + 1, // 0-based to 1-based
|
|
Column: columnCount,
|
|
Length: int(r.Len),
|
|
LineText: source.Contents[lineStart:lineEnd],
|
|
}
|
|
}
|
|
|
|
func detailStruct(data MsgData, terminalInfo TerminalInfo, maxMargin int) MsgDetail {
|
|
// Only highlight the first line of the line text
|
|
loc := *data.Location
|
|
endOfFirstLine := len(loc.LineText)
|
|
for i, c := range loc.LineText {
|
|
if c == '\r' || c == '\n' || c == '\u2028' || c == '\u2029' {
|
|
endOfFirstLine = i
|
|
break
|
|
}
|
|
}
|
|
firstLine := loc.LineText[:endOfFirstLine]
|
|
afterFirstLine := loc.LineText[endOfFirstLine:]
|
|
|
|
// Clamp values in range
|
|
if loc.Line < 0 {
|
|
loc.Line = 0
|
|
}
|
|
if loc.Column < 0 {
|
|
loc.Column = 0
|
|
}
|
|
if loc.Length < 0 {
|
|
loc.Length = 0
|
|
}
|
|
if loc.Column > endOfFirstLine {
|
|
loc.Column = endOfFirstLine
|
|
}
|
|
if loc.Length > endOfFirstLine-loc.Column {
|
|
loc.Length = endOfFirstLine - loc.Column
|
|
}
|
|
|
|
spacesPerTab := 2
|
|
lineText := renderTabStops(firstLine, spacesPerTab)
|
|
indent := strings.Repeat(" ", len(renderTabStops(firstLine[:loc.Column], spacesPerTab)))
|
|
marker := "^"
|
|
markerStart := len(indent)
|
|
markerEnd := len(indent)
|
|
|
|
// Extend markers to cover the full range of the error
|
|
if loc.Length > 0 {
|
|
markerEnd = len(renderTabStops(firstLine[:loc.Column+loc.Length], spacesPerTab))
|
|
}
|
|
|
|
// Clip the marker to the bounds of the line
|
|
if markerStart > len(lineText) {
|
|
markerStart = len(lineText)
|
|
}
|
|
if markerEnd > len(lineText) {
|
|
markerEnd = len(lineText)
|
|
}
|
|
if markerEnd < markerStart {
|
|
markerEnd = markerStart
|
|
}
|
|
|
|
// Trim the line to fit the terminal width
|
|
width := terminalInfo.Width
|
|
if width < 1 {
|
|
width = 80
|
|
}
|
|
width -= maxMargin + extraMarginChars
|
|
if width < 1 {
|
|
width = 1
|
|
}
|
|
if loc.Column == endOfFirstLine {
|
|
// If the marker is at the very end of the line, the marker will be a "^"
|
|
// character that extends one column past the end of the line. In this case
|
|
// we should reserve a column at the end so the marker doesn't wrap.
|
|
width -= 1
|
|
}
|
|
if len(lineText) > width {
|
|
// Try to center the error
|
|
sliceStart := (markerStart + markerEnd - width) / 2
|
|
if sliceStart > markerStart-width/5 {
|
|
sliceStart = markerStart - width/5
|
|
}
|
|
if sliceStart < 0 {
|
|
sliceStart = 0
|
|
}
|
|
if sliceStart > len(lineText)-width {
|
|
sliceStart = len(lineText) - width
|
|
}
|
|
sliceEnd := sliceStart + width
|
|
|
|
// Slice the line
|
|
slicedLine := lineText[sliceStart:sliceEnd]
|
|
markerStart -= sliceStart
|
|
markerEnd -= sliceStart
|
|
if markerStart < 0 {
|
|
markerStart = 0
|
|
}
|
|
if markerEnd > len(slicedLine) {
|
|
markerEnd = len(slicedLine)
|
|
}
|
|
|
|
// Truncate the ends with "..."
|
|
if len(slicedLine) > 3 && sliceStart > 0 {
|
|
slicedLine = "..." + slicedLine[3:]
|
|
if markerStart < 3 {
|
|
markerStart = 3
|
|
}
|
|
}
|
|
if len(slicedLine) > 3 && sliceEnd < len(lineText) {
|
|
slicedLine = slicedLine[:len(slicedLine)-3] + "..."
|
|
if markerEnd > len(slicedLine)-3 {
|
|
markerEnd = len(slicedLine) - 3
|
|
}
|
|
if markerEnd < markerStart {
|
|
markerEnd = markerStart
|
|
}
|
|
}
|
|
|
|
// Now we can compute the indent
|
|
indent = strings.Repeat(" ", markerStart)
|
|
lineText = slicedLine
|
|
}
|
|
|
|
// If marker is still multi-character after clipping, make the marker wider
|
|
if markerEnd-markerStart > 1 {
|
|
marker = strings.Repeat("~", markerEnd-markerStart)
|
|
}
|
|
|
|
// Put a margin before the marker indent
|
|
margin := marginWithLineText(maxMargin, loc.Line)
|
|
|
|
return MsgDetail{
|
|
Path: loc.File,
|
|
Line: loc.Line,
|
|
Column: loc.Column,
|
|
Message: data.Text,
|
|
|
|
SourceBefore: margin + lineText[:markerStart],
|
|
SourceMarked: lineText[markerStart:markerEnd],
|
|
SourceAfter: lineText[markerEnd:],
|
|
|
|
Indent: emptyMarginText(maxMargin) + indent,
|
|
Marker: marker,
|
|
|
|
ContentAfter: afterFirstLine,
|
|
}
|
|
}
|
|
|
|
func renderTabStops(withTabs string, spacesPerTab int) string {
|
|
if !strings.ContainsRune(withTabs, '\t') {
|
|
return withTabs
|
|
}
|
|
|
|
withoutTabs := strings.Builder{}
|
|
count := 0
|
|
|
|
for _, c := range withTabs {
|
|
if c == '\t' {
|
|
spaces := spacesPerTab - count%spacesPerTab
|
|
for i := 0; i < spaces; i++ {
|
|
withoutTabs.WriteRune(' ')
|
|
count++
|
|
}
|
|
} else {
|
|
withoutTabs.WriteRune(c)
|
|
count++
|
|
}
|
|
}
|
|
|
|
return withoutTabs.String()
|
|
}
|
|
|
|
func (log Log) AddError(source *Source, loc Loc, text string) {
|
|
log.AddMsg(Msg{
|
|
Kind: Error,
|
|
Data: RangeData(source, Range{Loc: loc}, text),
|
|
})
|
|
}
|
|
|
|
func (log Log) AddErrorWithNotes(source *Source, loc Loc, text string, notes []MsgData) {
|
|
log.AddMsg(Msg{
|
|
Kind: Error,
|
|
Data: RangeData(source, Range{Loc: loc}, text),
|
|
Notes: notes,
|
|
})
|
|
}
|
|
|
|
func (log Log) AddWarning(source *Source, loc Loc, text string) {
|
|
log.AddMsg(Msg{
|
|
Kind: Warning,
|
|
Data: RangeData(source, Range{Loc: loc}, text),
|
|
})
|
|
}
|
|
|
|
func (log Log) AddRangeError(source *Source, r Range, text string) {
|
|
log.AddMsg(Msg{
|
|
Kind: Error,
|
|
Data: RangeData(source, r, text),
|
|
})
|
|
}
|
|
|
|
func (log Log) AddRangeWarning(source *Source, r Range, text string) {
|
|
log.AddMsg(Msg{
|
|
Kind: Warning,
|
|
Data: RangeData(source, r, text),
|
|
})
|
|
}
|
|
|
|
func (log Log) AddRangeErrorWithNotes(source *Source, r Range, text string, notes []MsgData) {
|
|
log.AddMsg(Msg{
|
|
Kind: Error,
|
|
Data: RangeData(source, r, text),
|
|
Notes: notes,
|
|
})
|
|
}
|
|
|
|
func (log Log) AddRangeWarningWithNotes(source *Source, r Range, text string, notes []MsgData) {
|
|
log.AddMsg(Msg{
|
|
Kind: Warning,
|
|
Data: RangeData(source, r, text),
|
|
Notes: notes,
|
|
})
|
|
}
|
|
|
|
func RangeData(source *Source, r Range, text string) MsgData {
|
|
return MsgData{
|
|
Text: text,
|
|
Location: LocationOrNil(source, r),
|
|
}
|
|
}
|