From 67225d3ae5c7a0e186f9f276eefa2e2bd1429a45 Mon Sep 17 00:00:00 2001 From: Arpad Ryszka Date: Sun, 22 Nov 2020 19:14:45 +0100 Subject: [PATCH] fix line mode wrapping and raw text printing --- fprint.go | 111 ++++++++++++++++++++------- fprint_test.go => fprintmore_test.go | 8 +- notation.go | 37 +++++++-- reflect.go | 18 +++-- sprint_test.go | 58 +++++++++++--- 5 files changed, 178 insertions(+), 54 deletions(-) rename fprint_test.go => fprintmore_test.go (100%) diff --git a/fprint.go b/fprint.go index f1f37da..8c4b9b9 100644 --- a/fprint.go +++ b/fprint.go @@ -1,5 +1,7 @@ package notation +import "strings" + func unwrappable(n node) bool { return n.len == n.wrapLen.max && n.len == n.fullWrap.max @@ -29,6 +31,36 @@ func nodeLen(t int, n node) node { n.len += len(part) w += len(part) f += len(part) + case str: + // We assume here that an str is always contained by a node that has only a + // single str. + // + // If this changes in the future, then we need to provide tests for the + // additional cases. If this doesn't change anytime soon, then we can + // refactor this part. + // + n.len = len(part.val) + if part.raw == "" { + w = len(part.val) + f = len(part.val) + } else { + lines := strings.Split(part.raw, "\n") + part.rawLen.first = len(lines[0]) + for _, line := range lines { + if len(line) > part.rawLen.max { + part.rawLen.max = len(line) + } + } + + part.rawLen.last = len(lines[len(lines)-1]) + n.parts[i] = part + n.wrapLen.first = part.rawLen.first + n.fullWrap.first = part.rawLen.first + n.wrapLen.max = part.rawLen.max + n.fullWrap.max = part.rawLen.max + w = part.rawLen.last + f = part.rawLen.last + } case node: part = nodeLen(t, part) n.parts[i] = part @@ -100,7 +132,7 @@ func nodeLen(t int, n node) node { return n } -func wrapNode(t, c0, c1 int, n node) node { +func wrapNode(t, cf0, c0, c1 int, n node) node { if n.len <= c0 { return n } @@ -120,12 +152,32 @@ func wrapNode(t, c0, c1 int, n node) node { for i := 0; i < len(n.parts); i++ { p := n.parts[i] switch part := p.(type) { + case string: + cc0 -= len(part) + cc1 -= len(part) + if !trackBack && cc1 < 0 { + cc0 = 0 + cc1 = 0 + i = lastWrapperIndex + trackBack = true + } + case str: + // We assume here that an str is always contained by a node that has only a + // single str. Therefore we don't need to trackback to here, because the + // decision on wrapping was already made for the node. + // + // If this changes in the future, then we need to provide tests for the + // additional cases. If this doesn't change anytime soon, then we can + // refactor this part. + // + part.useRaw = part.raw != "" + n.parts[i] = part case node: - part = wrapNode(t, cc0, cc1, part) + part = wrapNode(t, cf0, cc0, cc1, part) n.parts[i] = part if part.wrap { - // This is an approximation: sometimes part.fullWrap.first is applied here, - // but usually those are the same. + // This is an approximation: sometimes part.fullWrap.first should be applied + // here, but usually those are the same. cc0 -= part.wrapLen.first cc1 -= part.wrapLen.first } else { @@ -149,13 +201,13 @@ func wrapNode(t, c0, c1 int, n node) node { lastWrapperIndex = i switch part.mode { case line: - c0, c1 = c0-t, c1-t + cl := cf0 - t var w int for j, ni := range part.items { - if w > 0 && w+len(part.sep)+ni.len > c0 { + if w > 0 && w+len(part.sep)+ni.len > cl { w = 0 - part.lineWrappers = append( - part.lineWrappers, + part.lineEnds = append( + part.lineEnds, j, ) } @@ -167,12 +219,13 @@ func wrapNode(t, c0, c1 int, n node) node { w += ni.len } + part.lineEnds = append(part.lineEnds, len(part.items)) n.parts[i] = part - c0, c1 = c0+t, c1+t default: for j := range part.items { part.items[j] = wrapNode( t, + cf0, c0-t, c1-t, part.items[j], @@ -201,48 +254,50 @@ func fprint(w *writer, t int, n node) { if !n.wrap { for i, ni := range part.items { - fprint(w, t, ni) - if i < len(part.items)-1 { + if i > 0 { w.write(part.sep) } + + fprint(w, t, ni) } continue } - t++ switch part.mode { case line: var ( - wi int - lineStarted bool + lines [][]node + last int ) - w.line(t) - for i, ni := range part.items { - if len(part.lineWrappers) > wi && - i == part.lineWrappers[wi] { - wi++ - w.line(t) - lineStarted = false - } + for _, i := range part.lineEnds { + lines = append(lines, part.items[last:i]) + last = i + } - if lineStarted { - w.write(part.sep) - } + for _, line := range lines { + w.blankLine() + w.tabs(1) + for i, ni := range line { + if i > 0 { + w.write(part.sep) + } - fprint(w, 0, ni) - lineStarted = true + fprint(w, 0, ni) + } } default: + t++ for _, ni := range part.items { w.line(t) fprint(w, t, ni) w.write(part.suffix) } + + t-- } - t-- w.line(t) default: w.write(part) diff --git a/fprint_test.go b/fprintmore_test.go similarity index 100% rename from fprint_test.go rename to fprintmore_test.go index a5508bf..59b1ed6 100644 --- a/fprint_test.go +++ b/fprintmore_test.go @@ -110,13 +110,13 @@ func TestFprint(t *testing.T) { t.Fatal(err) } - if n != len(expect) { - t.Fatalf("invalid write length; expected: %d, got: %d", len(expect), n) - } - if b.String() != expect { t.Fatalf("invalid output; expected: %s, got: %s", expect, b.String()) } + + if n != len(expect) { + t.Fatalf("invalid write length; expected: %d, got: %d", len(expect), n) + } }) t.Run("Fprintt", func(t *testing.T) { diff --git a/notation.go b/notation.go index a22cafd..c266a21 100644 --- a/notation.go +++ b/notation.go @@ -34,6 +34,13 @@ type node struct { parts []interface{} } +type str struct { + val string + raw string + useRaw bool + rawLen wrapLen +} + type wrapMode int const ( @@ -42,10 +49,10 @@ const ( ) type wrapper struct { - mode wrapMode - sep, suffix string - items []node - lineWrappers []int + mode wrapMode + sep, suffix string + items []node + lineEnds []int } type writer struct { @@ -61,6 +68,14 @@ func (n node) String() string { return b.String() } +func (s str) String() string { + if s.useRaw { + return s.raw + } + + return s.val +} + func (w *writer) write(o interface{}) { if w.err != nil { return @@ -71,13 +86,21 @@ func (w *writer) write(o interface{}) { w.err = err } -func (w *writer) line(t int) { +func (w *writer) blankLine() { w.write("\n") - for i := 0; i < t; i++ { +} + +func (w *writer) tabs(n int) { + for i := 0; i < n; i++ { w.write("\t") } } +func (w *writer) line(t int) { + w.blankLine() + w.tabs(t) +} + func nodeOf(parts ...interface{}) node { return node{parts: parts} } @@ -131,7 +154,7 @@ func fprintValues(w io.Writer, o opts, v []interface{}) (int, error) { n := reflectValue(o, reflect.ValueOf(vi)) if o&wrap != 0 { n = nodeLen(tab, n) - n = wrapNode(tab, cols0, cols1, n) + n = wrapNode(tab, cols0, cols0, cols1, n) } fprint(wr, 0, n) diff --git a/reflect.go b/reflect.go index 0b3a502..cc467cd 100644 --- a/reflect.go +++ b/reflect.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" "sort" + "strings" ) func withType(o opts) (opts, bool, bool) { @@ -140,6 +141,7 @@ func reflectMap(o opts, r reflect.Value) node { skeys []string ) + // TODO: simplify this when no sorting is required items := wrapper{sep: ", ", suffix: ","} itemOpts := o | skipTypes keys := r.MapKeys() @@ -201,7 +203,8 @@ func reflectList(o opts, r reflect.Value) node { } func reflectString(o opts, r reflect.Value) node { - b := []byte(r.String()) + sv := r.String() + b := []byte(sv) e := make([]byte, 0, len(b)) for _, c := range b { switch c { @@ -226,18 +229,23 @@ func reflectString(o opts, r reflect.Value) node { } } - s := fmt.Sprintf("\"%s\"", string(e)) + s := str{val: fmt.Sprintf("\"%s\"", string(e))} + if !strings.Contains(sv, "`") && strings.Contains(sv, "\n") { + s.raw = fmt.Sprintf("`%s`", sv) + } + + n := nodeOf(s) _, t, a := withType(o) if !t { - return nodeOf(s) + return n } tn := reflectType(r.Type()) if !a && tn.parts[0] == "string" { - return nodeOf(s) + return n } - return nodeOf(tn, "(", wrapper{items: []node{nodeOf(s)}}, ")") + return nodeOf(tn, "(", wrapper{items: []node{n}}, ")") } func reflectStruct(o opts, r reflect.Value) node { diff --git a/sprint_test.go b/sprint_test.go index dbb2a22..f7f29c6 100644 --- a/sprint_test.go +++ b/sprint_test.go @@ -126,8 +126,8 @@ func defaultSet() tests { {"nil custom list", struct{ l myList }{}, "{l: nil}"}, {"long item", []string{"foobarbazqux"}, "[]{\"foobarbazqux\"}"}, {"long subitem", []struct{ foo string }{{foo: "foobarbazqux"}}, "[]{{foo: \"foobarbazqux\"}}"}, - {"string", "\\\"\b\f\n\r\t\vfoo", "\"\\\\\\\"\\b\\f\\n\\r\\t\\vfoo\""}, - {"custom string", myString("\\\"\b\f\n\r\t\vfoo"), "\"\\\\\\\"\\b\\f\\n\\r\\t\\vfoo\""}, + {"string", "\\\"\b\f\r\t\vfoo", "\"\\\\\\\"\\b\\f\\r\\t\\vfoo\""}, + {"custom string", myString("\\\"\b\f\r\t\vfoo"), "\"\\\\\\\"\\b\\f\\r\\t\\vfoo\""}, {"structure", struct{ foo int }{42}, "{foo: 42}"}, {"custom structure", myStruct{42}, "{field: 42}"}, {"unsafe pointer", unsafe.Pointer(&struct{}{}), "pointer"}, @@ -178,7 +178,7 @@ func (t tests) expectTypes() tests { "nil custom list": "struct{l myList}{l: nil}", "long item": "[]string{\"foobarbazqux\"}", "long subitem": "[]struct{foo string}{{foo: \"foobarbazqux\"}}", - "custom string": "myString(\"\\\\\\\"\\b\\f\\n\\r\\t\\vfoo\")", + "custom string": "myString(\"\\\\\\\"\\b\\f\\r\\t\\vfoo\")", "structure": "struct{foo int}{foo: 42}", "custom structure": "myStruct{field: 42}", "unsafe pointer type": "struct{p Pointer}{p: nil}", @@ -214,7 +214,7 @@ func (t tests) expectVerboseTypes() tests { "nil custom list": "struct{l myList}{l: myList(nil)}", "long item": "[]string{string(\"foobarbazqux\")}", "long subitem": "[]struct{foo string}{struct{foo string}{foo: string(\"foobarbazqux\")}}", - "string": "string(\"\\\\\\\"\\b\\f\\n\\r\\t\\vfoo\")", + "string": "string(\"\\\\\\\"\\b\\f\\r\\t\\vfoo\")", "structure": "struct{foo int}{foo: int(42)}", "custom structure": "myStruct{field: interface{}(int(42))}", "unsafe pointer": "Pointer(pointer)", @@ -433,7 +433,7 @@ func (t tests) expectWrapAllWithTypes() tests { foo: "foobarbazqux", }, }`, - "custom string": "myString(\n\t\"\\\\\\\"\\b\\f\\n\\r\\t\\vfoo\"\n)", + "custom string": "myString(\n\t\"\\\\\\\"\\b\\f\\r\\t\\vfoo\"\n)", "structure": `struct{ foo int }{ @@ -474,7 +474,16 @@ func (t tests) expectOnlyLongWrappedWithTypes() tests { }{ i: nil, }`, - "nil map": `struct{m map[int]int}{ + "function with multiple return args": `func( + int, + int, +) ( + int, + int, +)`, + "nil map": `struct{ + m map[int]int +}{ m: nil, }`, "nil custom map": `struct{m myMap}{ @@ -498,7 +507,7 @@ func (t tests) expectOnlyLongWrappedWithTypes() tests { "long subitem": `[]struct{foo string}{ {foo: "foobarbazqux"}, }`, - "custom string": "myString(\n\t\"\\\\\\\"\\b\\f\\n\\r\\t\\vfoo\"\n)", + "custom string": "myString(\n\t\"\\\\\\\"\\b\\f\\r\\t\\vfoo\"\n)", "structure": `struct{foo int}{ foo: 42, }`, @@ -630,8 +639,8 @@ func (t tests) expectWrapAllWithVerboseTypes() tests { ), }, }`, - "string": "string(\n\t\"\\\\\\\"\\b\\f\\n\\r\\t\\vfoo\"\n)", - "custom string": "myString(\n\t\"\\\\\\\"\\b\\f\\n\\r\\t\\vfoo\"\n)", + "string": "string(\n\t\"\\\\\\\"\\b\\f\\r\\t\\vfoo\"\n)", + "custom string": "myString(\n\t\"\\\\\\\"\\b\\f\\r\\t\\vfoo\"\n)", "structure": `struct{ foo int }{ @@ -941,7 +950,7 @@ func TestBytes(t *testing.T) { }) } -func TestNonWrapperNodes(t *testing.T) { +func TestLongNonWrapperNodes(t *testing.T) { const expect = `map[struct{ foo int bar int @@ -963,3 +972,32 @@ func TestNonWrapperNodes(t *testing.T) { t.Fatalf("expected: %s, got: %s", expect, s) } } + +func TestSingleLongString(t *testing.T) { + t.Run("no line breaks", func(t *testing.T) { + const expect = `"foobarbazquxquuxquzquuz"` + defer withEnv(t, "TABWIDTH=2", "LINEWIDTH=9", "LINEWIDTH1=12")() + s := Sprintwt("foobarbazquxquuxquzquuz") + if s != expect { + t.Fatalf("expected: %s, got: %s", expect, s) + } + }) + + t.Run("line break and backquote", func(t *testing.T) { + const expect = "\"foobarbazqux`\\nquuxquzquuz\"" + defer withEnv(t, "TABWIDTH=2", "LINEWIDTH=9", "LINEWIDTH1=12")() + s := Sprintwt("foobarbazqux`\nquuxquzquuz") + if s != expect { + t.Fatalf("expected: %s, got: %s", expect, s) + } + }) + + t.Run("line break and no backquote", func(t *testing.T) { + const expect = "`foobarbazqux\nquuxquzquuz`" + defer withEnv(t, "TABWIDTH=2", "LINEWIDTH=9", "LINEWIDTH1=12")() + s := Sprintwt("foobarbazqux\nquuxquzquuz") + if s != expect { + t.Fatalf("expected: %s, got: %s", expect, s) + } + }) +}