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 (
	
	
	
	
	
	
	
	
)

const defaultTerminalWidth = 80

type Log struct {
	AddMsg    func(Msg)
	HasErrors func() bool
This is called after the build has finished but before writing to stdout. It exists to ensure that deferred warning messages end up in the terminal before the data written to stdout.
	AlmostDone func()

	Done func() []Msg
}

type LogLevel int8

const (
	LevelNone LogLevel = iota
	LevelInfo
	LevelWarning
	LevelError
	LevelSilent
)

type MsgKind uint8

const (
	Error MsgKind = iota
	Warning
	Note
)

func ( MsgKind) () string {
	switch  {
	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
Optional user-specified data that is passed through unmodified
	UserDetail interface{}
}

type MsgLocation struct {
	File       string
	Namespace  string
	Line       int // 1-based
	Column     int // 0-based, in bytes
	Length     int // in bytes
	LineText   string
	Suggestion string
}

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 ( Range) () int32 {
	return .Loc.Start + .Len
}
This type is just so we can use Go's native sort function
type SortableMsgs []Msg

func ( SortableMsgs) () int          { return len() }
func ( SortableMsgs) ( int,  int) { [], [] = [], [] }

func ( SortableMsgs) ( int,  int) bool {
	 := []
	 := []
	 := .Data.Location
	 := .Data.Location
	if  == nil ||  == nil {
		return  == nil &&  != nil
	}
	if .File != .File {
		return .File < .File
	}
	if .Line != .Line {
		return .Line < .Line
	}
	if .Column != .Column {
		return .Column < .Column
	}
	if .Kind != .Kind {
		return .Kind < .Kind
	}
	return .Data.Text < .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.
This feature was added to support ancient CSS libraries that append things like "?#iefix" and "#icons" to some of their import paths as a hack for IE6. The intent is for these suffix parts to be ignored but passed through to the output. This is supported by other bundlers, so we also support this.
This corresponds to a value of "false' in the "browser" package.json field
	PathDisabled PathFlags = 1 << iota
)

func ( Path) () bool {
	return (.Flags & PathDisabled) != 0
}

func ( Path) ( Path) bool {
	return .Namespace > .Namespace ||
		(.Namespace == .Namespace && (.Text < .Text ||
			(.Text == .Text && (.Flags < .Flags ||
				(.Flags == .Flags && .IgnoredSuffix < .IgnoredSuffix)))))
}
This has a custom implementation instead of using "filepath.Dir/Base/Ext" because it should work the same on Unix and Windows. These names end up in the generated output and the generated output should not depend on the OS.
func ( string) ( string,  string,  string) {
	for {
		 := strings.LastIndexAny(, "/\\")
Stop if there are no more slashes
		if  < 0 {
			 = 
			break
		}
Stop if we found a non-trailing slash
		if +1 != len() {
			,  = [:], [+1:]
			break
		}
Ignore trailing slashes
		 = [:]
	}
Strip off the extension
	if  := strings.LastIndexByte(, '.');  >= 0 {
		,  = [:], [:]
	}

	return
}

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.
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.
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 ( *Source) ( Range) string {
	return .Contents[.Loc.Start : .Loc.Start+.Len]
}

func ( *Source) ( Loc,  string) Range {
	 := .Contents[:.Start]
	 := strings.LastIndex(, )
	if  >= 0 {
		return Range{Loc: Loc{Start: int32()}, Len: int32(len())}
	}
	return Range{Loc: }
}

func ( *Source) ( Loc,  string) Range {
	 := .Contents[.Start:]
	 := strings.Index(, )
	if  >= 0 {
		return Range{Loc: Loc{Start: .Start + int32()}, Len: int32(len())}
	}
	return Range{Loc: }
}

func ( *Source) ( Loc) Range {
	 := .Contents[.Start:]
	if len() == 0 {
		return Range{Loc: , Len: 0}
	}

	 := [0]
Search for the matching quote character
		for  := 1;  < len(); ++ {
			 := []
			if  ==  {
				return Range{Loc: , Len: int32( + 1)}
			} else if  == '\\' {
				 += 1
			}
		}
	}

	return Range{Loc: , Len: 0}
}

func ( *Source) ( Loc) ( Range) {
	 := .Contents[.Start:]
	 = Range{Loc: , Len: 0}

	if len() > 0 {
		if  := [0];  >= '0' &&  <= '9' {
			.Len = 1
			for int(.Len) < len() {
				 := [.Len]
				if ( < '0' ||  > '9') && ( < 'a' ||  > 'z') && ( < 'A' ||  > 'Z') &&  != '.' &&  != '_' {
					break
				}
				.Len++
			}
		}
	}
	return
}

func ( *Source) ( Loc) ( Range) {
	 := .Contents[.Start:]
	 = Range{Loc: , Len: 0}

	if len() >= 2 && [0] == '\\' {
		.Len = 2
		for .Len < 4 && int(.Len) < len() {
			 := [.Len]
			if  < '0' ||  > '9' {
				break
			}
			.Len++
		}
	}
	return
}

func ( string,  int,  int,  bool) string {
	var  string
	if  == 1 {
		 = fmt.Sprintf("%d %s", , )
	} else {
		 = fmt.Sprintf("%d %ss", , )
	}
	if  <  {
		 = fmt.Sprintf("%d of %s", , )
	} else if  &&  > 1 {
		 = "all " + 
	}
	return 
}

func ( int,  int,  int,  int) string {
	 :=  <  ||  < 
	switch {
	case  == 0:
		return plural("warning", , , )
	case  == 0:
		return plural("error", , , )
	default:
		return fmt.Sprintf("%s and %s",
			plural("warning", , , ),
			plural("error", , , ))
	}
}

type TerminalInfo struct {
	IsTTY           bool
	UseColorEscapes bool
	Width           int
	Height          int
}

func ( OutputOptions) Log {
	var  sync.Mutex
	var  SortableMsgs
	 := GetTerminalInfo(os.Stderr)
	 := 0
	 := 0
	 := 0
	 := 0
	 := false
	 := .MessageLimit
	if  == 0 {
		 = 0x7FFFFFFF
	}
	var  []Msg
	 := false

	 := func() {
		if  {
			return
		}
		 = true
Print the deferred warning now if there was no error after all
		for  > 0 && len() > 0 {
			++
			writeStringWithColor(os.Stderr, [0].String(, ))
			 = [1:]
			--
		}
Print out a summary
		if .MessageLimit > 0 && + > .MessageLimit {
			writeStringWithColor(os.Stderr, fmt.Sprintf("%s shown (disable the message limit with --error-limit=0)\n",
				errorAndWarningSummary(, , , )))
		} else if .LogLevel <= LevelInfo && ( != 0 ||  != 0) {
			writeStringWithColor(os.Stderr, fmt.Sprintf("%s\n",
				errorAndWarningSummary(, , , )))
		}
	}

	switch .Color {
	case ColorNever:
		.UseColorEscapes = false
	case ColorAlways:
		.UseColorEscapes = SupportsColorEscapes
	}

	return Log{
		AddMsg: func( Msg) {
			.Lock()
			defer .Unlock()
			 = append(, )

			switch .Kind {
			case Error:
				 = true
				if .LogLevel <= LevelError {
					++
				}
			case Warning:
				if .LogLevel <= LevelWarning {
					++
				}
			}
Be silent if we're past the limit so we don't flood the terminal
			if  == 0 {
				return
			}

			switch .Kind {
			case Error:
				if .LogLevel <= LevelError {
					++
					writeStringWithColor(os.Stderr, .String(, ))
					--
				}

			case Warning:
				if .LogLevel <= LevelWarning {
					if  > (.MessageLimit+1)/2 {
						++
						writeStringWithColor(os.Stderr, .String(, ))
						--
If we have less than half of the slots left, wait for potential future errors instead of using up all of the slots with warnings. We want the log for a failed build to always have at least one error in it.
						 = append(, )
					}
				}
			}
		},
		HasErrors: func() bool {
			.Lock()
			defer .Unlock()
			return 
		},
		AlmostDone: func() {
			.Lock()
			defer .Unlock()

			()
		},
		Done: func() []Msg {
			.Lock()
			defer .Unlock()

			()
			sort.Stable()
			return 
		},
	}
}

func ( []string,  string) {
	PrintMessageToStderr(, Msg{Kind: Error, Data: MsgData{Text: }})
}

func ( []string) OutputOptions {
	 := OutputOptions{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 ,  := range  {
		switch  {
		case "--color=false":
			.Color = ColorNever
		case "--color=true":
			.Color = ColorAlways
		case "--log-level=info":
			.LogLevel = LevelInfo
		case "--log-level=warning":
			.LogLevel = LevelWarning
		case "--log-level=error":
			.LogLevel = LevelError
		case "--log-level=silent":
			.LogLevel = LevelSilent
		}
	}

	return 
}

func ( []string,  Msg) {
	 := NewStderrLog(OutputOptionsForArgs())
	.AddMsg()
	.Done()
}

type Colors struct {
	Default string
	Bold    string
	Dim     string

	Red   string
	Green string
	Blue  string

	Cyan    string
	Magenta string
	Yellow  string

	Underline string
}

func ( *os.File,  LogLevel,  []string,  func(Colors) string) {
	 := OutputOptionsForArgs()
Skip logging these if these logs are disabled
	if .LogLevel >  {
		return
	}

	PrintTextWithColor(, .Color, )
}

func ( *os.File,  UseColor,  func(Colors) string) {
	var  bool
	switch  {
	case ColorNever:
		 = false
	case ColorAlways:
		 = SupportsColorEscapes
	case ColorIfTerminal:
		 = GetTerminalInfo().UseColorEscapes
	}

	var  Colors
	if  {
		.Default = colorReset
		.Bold = colorResetBold
		.Dim = colorResetDim

		.Red = colorRed
		.Green = colorGreen
		.Blue = colorBlue

		.Cyan = colorCyan
		.Magenta = colorMagenta
		.Yellow = colorYellow

		.Underline = colorResetUnderline
	}
	writeStringWithColor(, ())
}

type SummaryTableEntry struct {
	Dir         string
	Base        string
	Size        string
	Bytes       int
	IsSourceMap bool
}
This type is just so we can use Go's native sort function
type SummaryTable []SummaryTableEntry

func ( SummaryTable) () int          { return len() }
func ( SummaryTable) ( int,  int) { [], [] = [], [] }

func ( SummaryTable) ( int,  int) bool {
	 := []
	 := []
Sort source maps last
	if !.IsSourceMap && .IsSourceMap {
		return true
	}
	if .IsSourceMap && !.IsSourceMap {
		return false
	}
Sort by size first
	if .Bytes > .Bytes {
		return true
	}
	if .Bytes < .Bytes {
		return false
	}
Sort subdirectories first
	if strings.HasPrefix(.Dir, .Dir) {
		return true
	}
	if strings.HasPrefix(.Dir, .Dir) {
		return false
	}
Sort alphabetically by directory first
	if .Dir < .Dir {
		return true
	}
	if .Dir > .Dir {
		return false
	}
Then sort alphabetically by file name
	return .Base < .Base
}
Show a warning icon next to output files that are 1mb or larger
const sizeWarningThreshold = 1024 * 1024

func ( []string,  SummaryTable,  time.Time) {
	PrintText(os.Stderr, LevelInfo, , func( Colors) string {
		 := false
		 := strings.Builder{}
Assume we are running in Windows Command Prompt if we're on Windows. If so, we can't use emoji because it won't be supported. Except we can still use emoji if the WT_SESSION environment variable is present because that means we're running in the new Windows Terminal instead.
		if runtime.GOOS == "windows" {
			 = true
			for ,  := range os.Environ() {
				if strings.HasPrefix(, "WT_SESSION=") {
					 = false
					break
				}
			}
		}

		if len() > 0 {
			 := GetTerminalInfo(os.Stderr)
Truncate the table in case it's really long
			 := .Height - 10
			if .Height == 0 {
				 = 20
			} else if  < 5 {
				 = 5
			}
			 := len()
			sort.Sort()
			if  >  {
				 = [:]
			}
Compute the maximum width of the size column
			 := 2
			 := false
			 := 0
			 := 0
			for ,  := range  {
				 := len(.Dir) + len(.Base)
				 := len(.Size) + 
				if  >  {
					 = 
				}
				if  >  {
					 = 
				}
				if !.IsSourceMap && .Bytes >= sizeWarningThreshold {
					 = true
				}
			}

			 := "  "
			 := .Width
			if  < 1 {
				 = defaultTerminalWidth
			}
			 -= 2 * len()
Add space for the warning icon
				 -= 2
			}
			if  > + {
				 =  + 
			}
			.WriteString("\n")

			for ,  := range  {
				,  := .Dir, .Base
				 :=  - 
Truncate the path with "..." to fit on one line
Trim the directory from the front, leaving the trailing slash
					if len() > 0 {
						 :=  - len() - 3
						if  < 1 {
							 = 1
						}
						 = "..." + [len()-:]
					}
Trim the file name from the back
					if len()+len() >  {
						 :=  - len() - 3
						if  < 0 {
							 = 0
						}
						 = [:] + "..."
					}
				}

				 :=  - len(.Size) - len() - len()
				if  < 0 {
					 = 0
				}
Put a warning next to the size if it's above a certain threshold
				 := .Cyan
				 := ""
				if !.IsSourceMap && .Bytes >= sizeWarningThreshold {
					 = .Yellow
Emoji don't work in Windows Command Prompt
					if ! {
						 = " ⚠️"
					}
				}

				.WriteString(fmt.Sprintf("%s%s%s%s%s%s%s%s%s%s%s\n",
					,
					.Dim,
					,
					.Bold,
					,
					.Default,
					strings.Repeat(" ", ),
					,
					.Size,
					,
					.Default,
				))
			}
Say how many remaining files are not shown
			if  >  {
				 := "s"
				if  == +1 {
					 = ""
				}
				.WriteString(fmt.Sprintf("%s%s...and %d more output file%s...%s\n", , .Dim, -, , .Default))
			}
		}

		 := "⚡ "
Emoji don't work in Windows Command Prompt
		if  {
			 = ""
		}

		.WriteString(fmt.Sprintf("\n%s%sDone in %dms%s\n\n",
			,
			.Green,
			time.Since().Milliseconds(),
			.Default,
		))
		return .String()
	})
}

func () Log {
	var  SortableMsgs
	var  sync.Mutex
	var  bool

	return Log{
		AddMsg: func( Msg) {
			.Lock()
			defer .Unlock()
			if .Kind == Error {
				 = true
			}
			 = append(, )
		},
		HasErrors: func() bool {
			.Lock()
			defer .Unlock()
			return 
		},
		AlmostDone: func() {
		},
		Done: func() []Msg {
			.Lock()
			defer .Unlock()
			sort.Stable()
			return 
		},
	}
}

const colorReset = "\033[0m"

const colorRed = "\033[31m"
const colorGreen = "\033[32m"
const colorBlue = "\033[34m"

const colorCyan = "\033[36m"
const colorMagenta = "\033[35m"
const colorYellow = "\033[33m"

const colorResetDim = "\033[0;37m"
const colorBold = "\033[1m"
const colorResetBold = "\033[0;1m"
const colorResetUnderline = "\033[0;4m"

type UseColor uint8

const (
	ColorIfTerminal UseColor = iota
	ColorNever
	ColorAlways
)

type OutputOptions struct {
	IncludeSource bool
	MessageLimit  int
	Color         UseColor
	LogLevel      LogLevel
}

Compute the maximum margin
	 := 0
	if .IncludeSource {
		if .Data.Location != nil {
			 = len(fmt.Sprintf("%d", .Data.Location.Line))
		}
		for ,  := range .Notes {
			if .Location != nil {
				 := len(fmt.Sprintf("%d", .Location.Line))
				if  >  {
					 = 
				}
			}
		}
	}
Format the message
	 := msgString(, , .Kind, .Data, )
Put a blank line between the message and the notes if the message has a stack trace
	 := ""
	if  := .Data.Location;  != nil && strings.ContainsRune(.LineText, '\n') {
		 = "\n"
	}
Format the notes
	for ,  := range .Notes {
		 += 
		 += msgString(, , Note, , )
	}
Add extra spacing between messages if source code is present
	if .IncludeSource {
		 += "\n"
	}
	return 
}
The number of margin characters in addition to the line number
const extraMarginChars = 7

func ( int,  int) string {
	 := fmt.Sprintf("%d", )
	return fmt.Sprintf("    %s%s │ ", strings.Repeat(" ", -len()), )
}

func ( int,  bool) string {
	 := strings.Repeat(" ", )
	if  {
		return fmt.Sprintf("    %s ╵ ", )
	}
	return fmt.Sprintf("    %s │ ", )
}

func ( OutputOptions,  TerminalInfo,  MsgKind,  MsgData,  int) string {
	var  string
	 := colorBold
	 := colorResetBold
	 := ""

	if .IncludeSource {
		 = " > "
	}

	switch  {
	case Error:
		 = colorRed

	case Warning:
		 = colorMagenta

	case Note:
		 = colorReset
		 = colorResetBold
		 = colorReset
		if .IncludeSource {
			 = "   "
		}

	default:
		panic("Internal error")
	}

	if .Location == nil {
		if .UseColorEscapes {
			return fmt.Sprintf("%s%s%s%s: %s%s%s\n",
				, , , .String(),
				, .Text,
				colorReset)
		}

		return fmt.Sprintf("%s%s: %s\n", , .String(), .Text)
	}

	if !.IncludeSource {
		if .UseColorEscapes {
			return fmt.Sprintf("%s%s%s: %s%s: %s%s%s\n",
				, , .Location.File,
				, .String(),
				, .Text,
				colorReset)
		}

		return fmt.Sprintf("%s%s: %s: %s\n",
			, .Location.File, .String(), .Text)
	}

	 := detailStruct(, , )

	if .UseColorEscapes {
		if .Suggestion != "" {
			return fmt.Sprintf("%s%s%s:%d:%d: %s%s: %s%s\n%s%s%s%s%s%s\n%s%s%s%s%s\n%s%s%s%s%s%s%s\n",
				, , .Path, .Line, .Column,
				, .String(),
				, .Message,
				colorResetDim, .SourceBefore, colorGreen, .SourceMarked, colorResetDim, .SourceAfter,
				emptyMarginText(, false), .Indent, colorGreen, .Marker, colorResetDim,
				emptyMarginText(, true), .Indent, colorGreen, .Suggestion, colorResetDim,
				.ContentAfter, colorReset)
		}

		return fmt.Sprintf("%s%s%s:%d:%d: %s%s: %s%s\n%s%s%s%s%s%s\n%s%s%s%s%s%s%s\n",
			, , .Path, .Line, .Column,
			, .String(),
			, .Message,
			colorResetDim, .SourceBefore, colorGreen, .SourceMarked, colorResetDim, .SourceAfter,
			emptyMarginText(, true), .Indent, colorGreen, .Marker, colorResetDim,
			.ContentAfter, colorReset)
	}

	if .Suggestion != "" {
		return fmt.Sprintf("%s%s:%d:%d: %s: %s\n%s%s%s\n%s%s%s\n%s%s%s%s\n",
			, .Path, .Line, .Column,
			.String(), .Message,
			.SourceBefore, .SourceMarked, .SourceAfter,
			emptyMarginText(, false), .Indent, .Marker,
			emptyMarginText(, true), .Indent, .Suggestion,
			.ContentAfter)
	}

	return fmt.Sprintf("%s%s:%d:%d: %s: %s\n%s%s%s\n%s%s%s%s\n",
		, .Path, .Line, .Column,
		.String(), .Message,
		.SourceBefore, .SourceMarked, .SourceAfter,
		emptyMarginText(, true), .Indent, .Marker,
		.ContentAfter)
}

type MsgDetail struct {
	Path    string
	Line    int
	Column  int
	Message string

	SourceBefore string
	SourceMarked string
	SourceAfter  string

	Indent     string
	Marker     string
	Suggestion string

	ContentAfter string
}

func ( string,  int) ( int,  int,  int,  int) {
	var  rune
	if  > len() {
		 = len()
	}
Scan up to the offset and count lines
	for ,  := range [:] {
		switch  {
		case '\n':
			 =  + 1
			if  != '\r' {
				++
			}
		case '\r':
			 =  + 1
			++
		case '\u2028', '\u2029':
			 =  + 3 // These take three bytes to encode in UTF-8
			++
		}
		 = 
	}
Scan to the end of the line (or end of file if this is the last line)
	 = len()
:
	for ,  := range [:] {
		switch  {
		case '\r', '\n', '\u2028', '\u2029':
			 =  + 
			break 
		}
	}

	 =  - 
	return
}

func ( *Source,  Range) *MsgLocation {
	if  == nil {
		return nil
	}
Convert the index into a line and column number
	, , ,  := computeLineAndColumn(.Contents, int(.Loc.Start))

	return &MsgLocation{
		File:     .PrettyPath,
		Line:      + 1, // 0-based to 1-based
		Column:   ,
		Length:   int(.Len),
		LineText: .Contents[:],
	}
}

Only highlight the first line of the line text
	 := *.Location
	 := len(.LineText)
	for ,  := range .LineText {
		if  == '\r' ||  == '\n' ||  == '\u2028' ||  == '\u2029' {
			 = 
			break
		}
	}
	 := .LineText[:]
	 := .LineText[:]
Clamp values in range
	if .Line < 0 {
		.Line = 0
	}
	if .Column < 0 {
		.Column = 0
	}
	if .Length < 0 {
		.Length = 0
	}
	if .Column >  {
		.Column = 
	}
	if .Length > -.Column {
		.Length =  - .Column
	}

	 := 2
	 := renderTabStops(, )
	 := renderTabStops([:.Column], )
	 := len()
	 := 
	 := strings.Repeat(" ", estimateWidthInTerminal())
	 := "^"
Extend markers to cover the full range of the error
	if .Length > 0 {
		 = len(renderTabStops([:.Column+.Length], ))
	}
Clip the marker to the bounds of the line
	if  > len() {
		 = len()
	}
	if  > len() {
		 = len()
	}
	if  <  {
		 = 
	}
Trim the line to fit the terminal width
	 := .Width
	if  < 1 {
		 = defaultTerminalWidth
	}
	 -=  + extraMarginChars
	if  < 1 {
		 = 1
	}
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.
		 -= 1
	}
Try to center the error
		 := ( +  - ) / 2
		if  > -/5 {
			 =  - /5
		}
		if  < 0 {
			 = 0
		}
		if  > len()- {
			 = len() - 
		}
		 :=  + 
Slice the line
		 := [:]
		 -= 
		 -= 
		if  < 0 {
			 = 0
		}
		if  > len() {
			 = len()
		}
Truncate the ends with "..."
		if len() > 3 &&  > 0 {
			 = "..." + [3:]
			if  < 3 {
				 = 3
			}
		}
		if len() > 3 &&  < len() {
			 = [:len()-3] + "..."
			if  > len()-3 {
				 = len() - 3
			}
			if  <  {
				 = 
			}
		}
Now we can compute the indent
		 = 
		 = strings.Repeat(" ", estimateWidthInTerminal([:]))
	}
If marker is still multi-character after clipping, make the marker wider
	if - > 1 {
		 = strings.Repeat("~", estimateWidthInTerminal([:]))
	}
Put a margin before the marker indent
	 := marginWithLineText(, .Line)

	return MsgDetail{
		Path:    .File,
		Line:    .Line,
		Column:  .Column,
		Message: .Text,

		SourceBefore:  + [:],
		SourceMarked: [:],
		SourceAfter:  [:],

		Indent:     ,
		Marker:     ,
		Suggestion: .Suggestion,

		ContentAfter: ,
	}
}
Estimate the number of columns this string will take when printed
For now just assume each code point is one column. This is wrong but is less wrong than assuming each code unit is one column.
	 := 0
	for  != "" {
		,  := utf8.DecodeRuneInString()
		 = [:]
Ignore the Zero Width No-Break Space character (UTF-8 BOM)
		if  != 0xFEFF {
			++
		}
	}
	return 
}

func ( string,  int) string {
	if !strings.ContainsRune(, '\t') {
		return 
	}

	 := strings.Builder{}
	 := 0

	for ,  := range  {
		if  == '\t' {
			 :=  - %
			for  := 0;  < ; ++ {
				.WriteRune(' ')
				++
			}
		} else {
			.WriteRune()
			++
		}
	}

	return .String()
}

func ( Log) ( *Source,  Loc,  string) {
	.AddMsg(Msg{
		Kind: Error,
		Data: RangeData(, Range{Loc: }, ),
	})
}

func ( Log) ( *Source,  Loc,  string,  []MsgData) {
	.AddMsg(Msg{
		Kind:  Error,
		Data:  RangeData(, Range{Loc: }, ),
		Notes: ,
	})
}

func ( Log) ( *Source,  Loc,  string) {
	.AddMsg(Msg{
		Kind: Warning,
		Data: RangeData(, Range{Loc: }, ),
	})
}

func ( Log) ( *Source,  Range,  string) {
	.AddMsg(Msg{
		Kind: Error,
		Data: RangeData(, , ),
	})
}

func ( Log) ( *Source,  Range,  string) {
	.AddMsg(Msg{
		Kind: Warning,
		Data: RangeData(, , ),
	})
}

func ( Log) ( *Source,  Range,  string,  []MsgData) {
	.AddMsg(Msg{
		Kind:  Error,
		Data:  RangeData(, , ),
		Notes: ,
	})
}

func ( Log) ( *Source,  Range,  string,  []MsgData) {
	.AddMsg(Msg{
		Kind:  Warning,
		Data:  RangeData(, , ),
		Notes: ,
	})
}

func ( *Source,  Range,  string) MsgData {
	return MsgData{
		Text:     ,
		Location: LocationOrNil(, ),
	}