mirror of
https://github.com/go-sylixos/elvish.git
synced 2024-12-01 00:33:05 +08:00
pkg/diag: Include line:col of start position in Error and Show methods.
The Error methods used to show the start and end indices, while the Show methods used to show line ranges. Showing line:col of the start position seems to be pretty standard; both Go and Rust do that. I tried including the line:col of the end position too, but can't find a good enough format.
This commit is contained in:
parent
1acc45cfc2
commit
78862465a4
|
@ -51,8 +51,7 @@ func (err cognateErrors) Error() string {
|
|||
if i > 0 {
|
||||
sb.WriteString("; ")
|
||||
}
|
||||
// TODO: Include line and column numbers instead of byte indices.
|
||||
fmt.Fprintf(&sb, "%d-%d: %s", e.Context.From, e.Context.To, e.Message)
|
||||
fmt.Fprintf(&sb, "%s: %s", e.Context.culprit.describeStart(), e.Message)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
@ -62,12 +61,12 @@ func (err cognateErrors) Show(indent string) string {
|
|||
fmt.Fprintf(&sb, "Multiple %ss in %s:", err[0].Type, err[0].Context.Name)
|
||||
for _, e := range err {
|
||||
sb.WriteString("\n" + indent + " ")
|
||||
fmt.Fprintf(&sb, "\033[31;1m%s\033[m", e.Message)
|
||||
sb.WriteString(messageStart + e.Message + messageEnd)
|
||||
sb.WriteString("\n" + indent + " ")
|
||||
// This duplicates part of [Context.ShowCompact].
|
||||
desc := e.Context.lineRange() + " "
|
||||
desc := e.Context.culprit.describeStart() + ": "
|
||||
descIndent := strings.Repeat(" ", wcwidth.Of(desc))
|
||||
sb.WriteString(desc + e.Context.relevantSource(indent+" "+descIndent))
|
||||
sb.WriteString(desc + e.Context.culprit.Show(indent+" "+descIndent))
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
|
|
@ -22,51 +22,33 @@ var (
|
|||
)
|
||||
|
||||
var cognateErrorsTests = []struct {
|
||||
name string
|
||||
errs []*Error
|
||||
wantError string
|
||||
wantShow string
|
||||
name string
|
||||
errs []*Error
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "no error",
|
||||
errs: nil,
|
||||
wantError: "",
|
||||
wantShow: "",
|
||||
name: "no error",
|
||||
errs: nil,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "one error",
|
||||
errs: []*Error{err1},
|
||||
wantError: err1.Error(),
|
||||
wantShow: err1.Show(""),
|
||||
name: "one error",
|
||||
errs: []*Error{err1},
|
||||
wantErr: err1,
|
||||
},
|
||||
{
|
||||
name: "multiple errors",
|
||||
errs: []*Error{err1, err2},
|
||||
wantError: "multiple foo errors in [test]: 5-8: bad 1; 5-8: bad 2",
|
||||
wantShow: lines(
|
||||
"Multiple foo errors in [test]:",
|
||||
" \x1b[31;1mbad 1\x1b[m",
|
||||
" line 1: echo \x1b[1;4m(1)\x1b[m",
|
||||
" \x1b[31;1mbad 2\x1b[m",
|
||||
" line 1: echo \x1b[1;4m(2)\x1b[m"),
|
||||
name: "multiple errors",
|
||||
errs: []*Error{err1, err2},
|
||||
wantErr: cognateErrors{err1, err2},
|
||||
},
|
||||
}
|
||||
|
||||
func TestCognateErrors(t *testing.T) {
|
||||
func TestPackAndUnpackCognateErrors(t *testing.T) {
|
||||
for _, tc := range cognateErrorsTests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := PackCognateErrors(tc.errs)
|
||||
if err == nil {
|
||||
if tc.wantError != "" || tc.wantShow != "" {
|
||||
t.Errorf("Want non-nil error, got nil")
|
||||
}
|
||||
} else {
|
||||
if got := err.Error(); got != tc.wantError {
|
||||
t.Errorf("Error() (-want +got):\n%s", cmp.Diff(tc.wantError, got))
|
||||
}
|
||||
if got := err.(Shower).Show(""); got != tc.wantShow {
|
||||
t.Errorf("Show() (-want +got):\n%s", cmp.Diff(tc.wantShow, got))
|
||||
}
|
||||
if !reflect.DeepEqual(err, tc.wantErr) {
|
||||
t.Errorf("got packed error %#v, want %#v", err, tc.wantErr)
|
||||
}
|
||||
unpacked := UnpackCognateErrors(err)
|
||||
if !reflect.DeepEqual(unpacked, tc.errs) {
|
||||
|
@ -82,3 +64,24 @@ func TestUnpackCognateErrors_CalledWithOtherErrorType(t *testing.T) {
|
|||
t.Errorf("want nil, got %v", unpacked)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCognateErrors(t *testing.T) {
|
||||
setCulpritMarkers(t, "<", ">")
|
||||
setMessageMarkers(t, "{", "}")
|
||||
err := PackCognateErrors([]*Error{err1, err2})
|
||||
wantError := "multiple foo errors in [test]: 1:6: bad 1; 1:6: bad 2"
|
||||
if s := err.Error(); s != wantError {
|
||||
t.Errorf(".Error() returns unexpected result (-want +got):\n%s",
|
||||
cmp.Diff(wantError, s))
|
||||
}
|
||||
wantShow := dedent(`
|
||||
Multiple foo errors in [test]:
|
||||
{bad 1}
|
||||
1:6: echo <(1)>
|
||||
{bad 2}
|
||||
1:6: echo <(2)>`)
|
||||
if show := err.(Shower).Show(""); show != wantShow {
|
||||
t.Errorf(".Show(\"\") returns unexpected result (-want +got):\n%s",
|
||||
cmp.Diff(wantShow, show))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package diag
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
|
@ -11,132 +10,102 @@ import (
|
|||
// Context is a range of text in a source code. It is typically used for
|
||||
// errors that can be associated with a part of the source code, like parse
|
||||
// errors and a traceback entry.
|
||||
//
|
||||
// Context values should only be constructed using [NewContext].
|
||||
type Context struct {
|
||||
Name string
|
||||
Source string
|
||||
Ranging
|
||||
|
||||
savedShowInfo *rangeShowInfo
|
||||
culprit culprit
|
||||
}
|
||||
|
||||
// NewContext creates a new Context.
|
||||
func NewContext(name, source string, r Ranger) *Context {
|
||||
return &Context{name, source, r.Range(), nil}
|
||||
rg := r.Range()
|
||||
return &Context{name, source, rg, makeCulprit(source, rg)}
|
||||
}
|
||||
|
||||
// Information about the source range that are needed for showing.
|
||||
type rangeShowInfo struct {
|
||||
// Head is the piece of text immediately before Culprit, extending to, but
|
||||
// not including the closest line boundary. If Culprit already starts after
|
||||
// a line boundary, Head is an empty string.
|
||||
// Show shows a SourceContext.
|
||||
func (c *Context) Show(indent string) string {
|
||||
return fmt.Sprintf("%s:%s:\n%s%s",
|
||||
c.Name, c.culprit.describeStart(), indent+" ", c.culprit.Show(indent+" "))
|
||||
}
|
||||
|
||||
// ShowCompact shows a Context, with no line break between the culprit range
|
||||
// description and relevant source excerpt.
|
||||
func (c *Context) ShowCompact(indent string) string {
|
||||
desc := fmt.Sprintf("%s:%s: ", c.Name, c.culprit.describeStart())
|
||||
// Extra indent so that following lines line up with the first line.
|
||||
descIndent := strings.Repeat(" ", wcwidth.Of(desc))
|
||||
return desc + c.culprit.Show(indent+descIndent)
|
||||
}
|
||||
|
||||
// Information about the lines that contain the culprit.
|
||||
type culprit struct {
|
||||
// The actual culprit text.
|
||||
Body string
|
||||
// Text before Body on its first line.
|
||||
Head string
|
||||
// Culprit is Source[Begin:End], with any trailing newlines stripped.
|
||||
Culprit string
|
||||
// Tail is the piece of text immediately after Culprit, extending to, but
|
||||
// not including the closet line boundary. If Culprit already ends before a
|
||||
// line boundary, Tail is an empty string.
|
||||
// Text after Body on its last line.
|
||||
Tail string
|
||||
// BeginLine is the (1-based) line number that the first character of Culprit is on.
|
||||
BeginLine int
|
||||
// EndLine is the (1-based) line number that the last character of Culprit is on.
|
||||
EndLine int
|
||||
// 1-based line and column numbers of the start position.
|
||||
StartLine, StartCol int
|
||||
}
|
||||
|
||||
// Variables controlling the style of the culprit.
|
||||
var (
|
||||
culpritLineBegin = "\033[1;4m"
|
||||
culpritLineEnd = "\033[m"
|
||||
culpritPlaceHolder = "^"
|
||||
)
|
||||
|
||||
func (c *Context) showInfo() *rangeShowInfo {
|
||||
if c.savedShowInfo != nil {
|
||||
return c.savedShowInfo
|
||||
}
|
||||
|
||||
before := c.Source[:c.From]
|
||||
culprit := c.Source[c.From:c.To]
|
||||
after := c.Source[c.To:]
|
||||
func makeCulprit(source string, r Ranging) culprit {
|
||||
before := source[:r.From]
|
||||
body := source[r.From:r.To]
|
||||
after := source[r.To:]
|
||||
|
||||
head := lastLine(before)
|
||||
beginLine := strings.Count(before, "\n") + 1
|
||||
fromLine := strings.Count(before, "\n") + 1
|
||||
fromCol := 1 + wcwidth.Of(head)
|
||||
|
||||
// If the culprit ends with a newline, stripe it. Otherwise, tail is nonempty.
|
||||
// If the culprit ends with a newline, stripe it, and tail is empty.
|
||||
// Otherwise, tail is nonempty.
|
||||
var tail string
|
||||
if strings.HasSuffix(culprit, "\n") {
|
||||
culprit = culprit[:len(culprit)-1]
|
||||
if strings.HasSuffix(body, "\n") {
|
||||
body = body[:len(body)-1]
|
||||
} else {
|
||||
tail = firstLine(after)
|
||||
}
|
||||
|
||||
endLine := beginLine + strings.Count(culprit, "\n")
|
||||
|
||||
c.savedShowInfo = &rangeShowInfo{head, culprit, tail, beginLine, endLine}
|
||||
return c.savedShowInfo
|
||||
return culprit{body, head, tail, fromLine, fromCol}
|
||||
}
|
||||
|
||||
// Show shows a SourceContext.
|
||||
func (c *Context) Show(sourceIndent string) string {
|
||||
if err := c.checkPosition(); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return (c.Name + ", " + c.lineRange() +
|
||||
"\n" + sourceIndent + c.relevantSource(sourceIndent))
|
||||
// Variables controlling the style of the culprit.
|
||||
var (
|
||||
culpritStart = "\033[1;4m"
|
||||
culpritEnd = "\033[m"
|
||||
culpritPlaceHolder = "^"
|
||||
)
|
||||
|
||||
func (cl *culprit) describeStart() string {
|
||||
return fmt.Sprintf("%d:%d", cl.StartLine, cl.StartCol)
|
||||
}
|
||||
|
||||
// ShowCompact shows a SourceContext, with no line break between the
|
||||
// source position range description and relevant source excerpt.
|
||||
func (c *Context) ShowCompact(sourceIndent string) string {
|
||||
if err := c.checkPosition(); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
desc := c.Name + ", " + c.lineRange() + " "
|
||||
// Extra indent so that following lines line up with the first line.
|
||||
descIndent := strings.Repeat(" ", wcwidth.Of(desc))
|
||||
return desc + c.relevantSource(sourceIndent+descIndent)
|
||||
}
|
||||
func (cl *culprit) Show(indent string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(cl.Head)
|
||||
|
||||
func (c *Context) checkPosition() error {
|
||||
if c.From == -1 {
|
||||
return fmt.Errorf("%s, unknown position", c.Name)
|
||||
} else if c.From < 0 || c.To > len(c.Source) || c.From > c.To {
|
||||
return fmt.Errorf("%s, invalid position %d-%d", c.Name, c.From, c.To)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Context) lineRange() string {
|
||||
info := c.showInfo()
|
||||
|
||||
if info.BeginLine == info.EndLine {
|
||||
return fmt.Sprintf("line %d:", info.BeginLine)
|
||||
}
|
||||
return fmt.Sprintf("line %d-%d:", info.BeginLine, info.EndLine)
|
||||
}
|
||||
|
||||
func (c *Context) relevantSource(sourceIndent string) string {
|
||||
info := c.showInfo()
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(info.Head)
|
||||
|
||||
culprit := info.Culprit
|
||||
if culprit == "" {
|
||||
culprit = culpritPlaceHolder
|
||||
body := cl.Body
|
||||
if body == "" {
|
||||
body = culpritPlaceHolder
|
||||
}
|
||||
|
||||
for i, line := range strings.Split(culprit, "\n") {
|
||||
for i, line := range strings.Split(body, "\n") {
|
||||
if i > 0 {
|
||||
buf.WriteByte('\n')
|
||||
buf.WriteString(sourceIndent)
|
||||
sb.WriteByte('\n')
|
||||
sb.WriteString(indent)
|
||||
}
|
||||
buf.WriteString(culpritLineBegin)
|
||||
buf.WriteString(line)
|
||||
buf.WriteString(culpritLineEnd)
|
||||
sb.WriteString(culpritStart)
|
||||
sb.WriteString(line)
|
||||
sb.WriteString(culpritEnd)
|
||||
}
|
||||
|
||||
buf.WriteString(info.Tail)
|
||||
return buf.String()
|
||||
sb.WriteString(cl.Tail)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func firstLine(s string) string {
|
||||
|
|
|
@ -18,69 +18,47 @@ var sourceRangeTests = []struct {
|
|||
Context: contextInParen("[test]", "echo (bad)"),
|
||||
Indent: "_",
|
||||
|
||||
WantShow: lines(
|
||||
"[test], line 1:",
|
||||
"_echo <(bad)>",
|
||||
),
|
||||
WantShowCompact: "[test], line 1: echo <(bad)>",
|
||||
WantShow: dedent(`
|
||||
[test]:1:6:
|
||||
_ echo <(bad)>`),
|
||||
WantShowCompact: "[test]:1:6: echo <(bad)>",
|
||||
},
|
||||
{
|
||||
Name: "multi-line culprit",
|
||||
Context: contextInParen("[test]", "echo (bad\nbad)\nmore"),
|
||||
Indent: "_",
|
||||
|
||||
WantShow: lines(
|
||||
"[test], line 1-2:",
|
||||
"_echo <(bad>",
|
||||
"_<bad)>",
|
||||
),
|
||||
WantShowCompact: lines(
|
||||
"[test], line 1-2: echo <(bad>",
|
||||
"_ <bad)>",
|
||||
),
|
||||
WantShow: dedent(`
|
||||
[test]:1:6:
|
||||
_ echo <(bad>
|
||||
_ <bad)>`),
|
||||
WantShowCompact: dedent(`
|
||||
[test]:1:6: echo <(bad>
|
||||
_ <bad)>`),
|
||||
},
|
||||
{
|
||||
Name: "trailing newline in culprit is removed",
|
||||
// 012345678 9
|
||||
Name: "trailing newline in culprit is removed",
|
||||
Context: NewContext("[test]", "echo bad\n", Ranging{5, 9}),
|
||||
Indent: "_",
|
||||
|
||||
WantShow: lines(
|
||||
"[test], line 1:",
|
||||
"_echo <bad>",
|
||||
),
|
||||
WantShowCompact: lines(
|
||||
"[test], line 1: echo <bad>",
|
||||
),
|
||||
WantShow: dedent(`
|
||||
[test]:1:6:
|
||||
_ echo <bad>`),
|
||||
WantShowCompact: "[test]:1:6: echo <bad>",
|
||||
},
|
||||
{
|
||||
Name: "empty culprit",
|
||||
// 012345
|
||||
Name: "empty culprit",
|
||||
Context: NewContext("[test]", "echo x", Ranging{5, 5}),
|
||||
|
||||
WantShow: lines(
|
||||
"[test], line 1:",
|
||||
"echo <^>x",
|
||||
),
|
||||
WantShowCompact: "[test], line 1: echo <^>x",
|
||||
},
|
||||
{
|
||||
Name: "unknown culprit range",
|
||||
Context: NewContext("[test]", "echo", Ranging{-1, -1}),
|
||||
WantShow: "[test], unknown position",
|
||||
WantShowCompact: "[test], unknown position",
|
||||
},
|
||||
{
|
||||
Name: "invalid culprit range",
|
||||
Context: NewContext("[test]", "echo", Ranging{2, 1}),
|
||||
WantShow: "[test], invalid position 2-1",
|
||||
WantShowCompact: "[test], invalid position 2-1",
|
||||
WantShow: dedent(`
|
||||
[test]:1:6:
|
||||
echo <^>x`),
|
||||
WantShowCompact: "[test]:1:6: echo <^>x",
|
||||
},
|
||||
}
|
||||
|
||||
func TestContext(t *testing.T) {
|
||||
culpritLineBegin = "<"
|
||||
culpritLineEnd = ">"
|
||||
setCulpritMarkers(t, "<", ">")
|
||||
for _, test := range sourceRangeTests {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
gotShow := test.Context.Show(test.Indent)
|
||||
|
|
|
@ -15,9 +15,8 @@ type Error struct {
|
|||
|
||||
// Error returns a plain text representation of the error.
|
||||
func (e *Error) Error() string {
|
||||
// TODO: Include line and column numbers instead of byte indices.
|
||||
return fmt.Sprintf("%s: %d-%d in %s: %s",
|
||||
e.Type, e.Context.From, e.Context.To, e.Context.Name, e.Message)
|
||||
return fmt.Sprintf("%s: %s:%s: %s",
|
||||
e.Type, e.Context.Name, e.Context.culprit.describeStart(), e.Message)
|
||||
}
|
||||
|
||||
// Range returns the range of the error.
|
||||
|
@ -25,8 +24,14 @@ func (e *Error) Range() Ranging {
|
|||
return e.Context.Range()
|
||||
}
|
||||
|
||||
var (
|
||||
messageStart = "\033[31;1m"
|
||||
messageEnd = "\033[m"
|
||||
)
|
||||
|
||||
// Show shows the error.
|
||||
func (e *Error) Show(indent string) string {
|
||||
header := fmt.Sprintf("%s: \033[31;1m%s\033[m\n", strutil.Title(e.Type), e.Message)
|
||||
return header + e.Context.ShowCompact(indent+" ")
|
||||
return fmt.Sprintf("%s: %s%s%s\n%s%s",
|
||||
strutil.Title(e.Type), messageStart, e.Message, messageEnd,
|
||||
indent+" ", e.Context.ShowCompact(indent+" "))
|
||||
}
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
package diag
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"src.elv.sh/pkg/testutil"
|
||||
)
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
testutil.Set(t, &culpritLineBegin, "<")
|
||||
testutil.Set(t, &culpritLineEnd, ">")
|
||||
setCulpritMarkers(t, "<", ">")
|
||||
setMessageMarkers(t, "{", "}")
|
||||
|
||||
err := &Error{
|
||||
Type: "some error",
|
||||
|
@ -17,7 +14,7 @@ func TestError(t *testing.T) {
|
|||
Context: *contextInParen("[test]", "echo (x)"),
|
||||
}
|
||||
|
||||
wantErrorString := "some error: 5-8 in [test]: bad list"
|
||||
wantErrorString := "some error: [test]:1:6: bad list"
|
||||
if gotErrorString := err.Error(); gotErrorString != wantErrorString {
|
||||
t.Errorf("Error() -> %q, want %q", gotErrorString, wantErrorString)
|
||||
}
|
||||
|
@ -27,16 +24,11 @@ func TestError(t *testing.T) {
|
|||
t.Errorf("Range() -> %v, want %v", gotRanging, wantRanging)
|
||||
}
|
||||
|
||||
wantShow := lines(
|
||||
// Type is capitalized in return value of Show
|
||||
"Some error: \033[31;1mbad list\033[m",
|
||||
"[test], line 1: echo <(x)>",
|
||||
)
|
||||
// Type is capitalized in return value of Show
|
||||
wantShow := dedent(`
|
||||
Some error: {bad list}
|
||||
[test]:1:6: echo <(x)>`)
|
||||
if gotShow := err.Show(""); gotShow != wantShow {
|
||||
t.Errorf("Show() -> %q, want %q", gotShow, wantShow)
|
||||
}
|
||||
}
|
||||
|
||||
func lines(lines ...string) string {
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
|
19
pkg/diag/testutil_test.go
Normal file
19
pkg/diag/testutil_test.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package diag
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"src.elv.sh/pkg/testutil"
|
||||
)
|
||||
|
||||
var dedent = testutil.Dedent
|
||||
|
||||
func setCulpritMarkers(t *testing.T, start, end string) {
|
||||
testutil.Set(t, &culpritStart, start)
|
||||
testutil.Set(t, &culpritEnd, end)
|
||||
}
|
||||
|
||||
func setMessageMarkers(t *testing.T, start, end string) {
|
||||
testutil.Set(t, &messageStart, start)
|
||||
testutil.Set(t, &messageEnd, end)
|
||||
}
|
|
@ -58,7 +58,7 @@ func TestCompile(t *testing.T) {
|
|||
Filter("[re '[']").
|
||||
DoesNotCompile("error parsing regexp: missing closing ]: `[`"),
|
||||
That("invalid syntax results in parse error").
|
||||
Filter("[and").DoesNotParse("parse error: 4-4 in [filter]: should be ']'"),
|
||||
Filter("[and").DoesNotParse("parse error: [filter]:1:5: should be ']'"),
|
||||
|
||||
// Unsupported for now, but may be in future
|
||||
That("options are not supported yet").
|
||||
|
|
|
@ -30,7 +30,7 @@ func TestHighlighter(t *testing.T) {
|
|||
f.TestTTY(t,
|
||||
"~> put $truex", Styles,
|
||||
" vvv ??????", term.DotHere, "\n",
|
||||
"compilation error: 4-10 in [interactive]: variable $truex not found",
|
||||
"compilation error: [interactive]:1:5: variable $truex not found",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -137,8 +137,7 @@ func (cp *compiler) deprecate(r diag.Ranger, msg string, minLevel int) {
|
|||
if prog.DeprecationLevel >= minLevel && cp.deprecations.register(dep) {
|
||||
err := diag.Error{
|
||||
Type: "deprecation", Message: msg,
|
||||
Context: diag.Context{
|
||||
Name: cp.srcMeta.Name, Source: cp.srcMeta.Code, Ranging: r.Range()}}
|
||||
Context: *diag.NewContext(cp.srcMeta.Name, cp.srcMeta.Code, r.Range())}
|
||||
fmt.Fprintln(cp.warn, err.Show(""))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@ func (exc *exception) Show(indent string) string {
|
|||
buf.WriteString(indent + "Traceback:")
|
||||
for tb := exc.stackTrace; tb != nil; tb = tb.Next {
|
||||
buf.WriteString("\n" + indent + " ")
|
||||
buf.WriteString(tb.Head.Show(indent + " "))
|
||||
buf.WriteString(tb.Head.Show(indent + " "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user