github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/util/pretty/util.go (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package pretty
    12  
    13  // This file contains utility and other non-standard functions not listed
    14  // in the paper (see the package documentation). Utility functions are those
    15  // that are generally useful or need to create certain structures (like union)
    16  // in a correct way.
    17  
    18  // Join joins Docs d with string s and Line. There is no space between each
    19  // item in d and the subsequent instance of s.
    20  func Join(s string, d ...Doc) Doc {
    21  	return JoinDoc(Concat(Text(s), Line), d...)
    22  }
    23  
    24  // JoinDoc joins Docs d with Doc s.
    25  func JoinDoc(s Doc, d ...Doc) Doc {
    26  	switch len(d) {
    27  	case 0:
    28  		return Nil
    29  	case 1:
    30  		return d[0]
    31  	default:
    32  		return Fold(Concat, d[0], s, JoinDoc(s, d[1:]...))
    33  	}
    34  }
    35  
    36  // JoinNestedRight nests nested with string s.
    37  // Every item after the first is indented.
    38  // For example:
    39  // aaaa
    40  // <sep> bbb
    41  //       bbb
    42  // <sep> ccc
    43  //       ccc
    44  func JoinNestedRight(sep Doc, nested ...Doc) Doc {
    45  	switch len(nested) {
    46  	case 0:
    47  		return Nil
    48  	case 1:
    49  		return nested[0]
    50  	default:
    51  		return Concat(
    52  			nested[0],
    53  			FoldMap(Concat,
    54  				func(a Doc) Doc { return Concat(Line, ConcatSpace(sep, NestT(Group(a)))) },
    55  				nested[1:]...))
    56  	}
    57  }
    58  
    59  // ConcatDoc concatenates two Docs with between.
    60  func ConcatDoc(a, b, between Doc) Doc {
    61  	return simplifyNil(a, b, func(a, b Doc) Doc {
    62  		return Concat(
    63  			a,
    64  			Concat(
    65  				between,
    66  				b,
    67  			),
    68  		)
    69  	})
    70  }
    71  
    72  // ConcatLine concatenates two Docs with a Line.
    73  func ConcatLine(a, b Doc) Doc {
    74  	return ConcatDoc(a, b, Line)
    75  }
    76  
    77  // ConcatSpace concatenates two Docs with a space.
    78  func ConcatSpace(a, b Doc) Doc {
    79  	return ConcatDoc(a, b, textSpace)
    80  }
    81  
    82  // Stack concats Docs with a Line between each.
    83  func Stack(d ...Doc) Doc {
    84  	return Fold(ConcatLine, d...)
    85  }
    86  
    87  // Fillwords fills lines with as many docs as will fit joined with a space or line.
    88  func Fillwords(d ...Doc) Doc {
    89  	u := &union{textSpace, line{}}
    90  	fill := func(a, b Doc) Doc {
    91  		return ConcatDoc(a, b, u)
    92  	}
    93  	return Fold(fill, d...)
    94  }
    95  
    96  // JoinGroupAligned nests joined d with divider under head.
    97  func JoinGroupAligned(head, divider string, d ...Doc) Doc {
    98  	return AlignUnder(Text(head), Join(divider, d...))
    99  }
   100  
   101  // NestUnder nests nested under head.
   102  func NestUnder(head, nested Doc) Doc {
   103  	return Group(Concat(
   104  		head,
   105  		NestT(Concat(
   106  			Line,
   107  			Group(nested),
   108  		)),
   109  	))
   110  }
   111  
   112  // AlignUnder aligns nested to the right of head, and, if
   113  // this does not fit on the line, nests nested under head.
   114  func AlignUnder(head, nested Doc) Doc {
   115  	g := Group(nested)
   116  	a := ConcatSpace(head, Align(g))
   117  	b := Concat(head, NestT(Concat(Line, g)))
   118  	return Group(&union{a, b})
   119  }
   120  
   121  // Fold applies f recursively to all Docs in d.
   122  func Fold(f func(a, b Doc) Doc, d ...Doc) Doc {
   123  	switch len(d) {
   124  	case 0:
   125  		return Nil
   126  	case 1:
   127  		return d[0]
   128  	default:
   129  		return f(d[0], Fold(f, d[1:]...))
   130  	}
   131  }
   132  
   133  // FoldMap applies f recursively to all Docs in d processed through g.
   134  func FoldMap(f func(a, b Doc) Doc, g func(Doc) Doc, d ...Doc) Doc {
   135  	switch len(d) {
   136  	case 0:
   137  		return Nil
   138  	case 1:
   139  		return g(d[0])
   140  	default:
   141  		return f(g(d[0]), FoldMap(f, g, d[1:]...))
   142  	}
   143  }
   144  
   145  // BracketDoc is like Bracket except it accepts Docs instead of strings.
   146  func BracketDoc(l, x, r Doc) Doc {
   147  	return Group(Fold(Concat,
   148  		l,
   149  		NestT(Concat(SoftBreak, x)),
   150  		SoftBreak,
   151  		r,
   152  	))
   153  }
   154  
   155  // simplifyNil returns fn(a, b). If either Doc is Nil, the other Doc
   156  // is returned without invoking fn.
   157  func simplifyNil(a, b Doc, fn func(Doc, Doc) Doc) Doc {
   158  	if a == Nil {
   159  		return b
   160  	}
   161  	if b == Nil {
   162  		return a
   163  	}
   164  	return fn(a, b)
   165  }
   166  
   167  // JoinNestedOuter attempts to de-indent the items after the first so
   168  // that the operator appears in the right margin. This replacement is
   169  // only done if there are enough "simple spaces" (as per previous uses
   170  // of Align) to de-indent. No attempt is made to de-indent hard tabs,
   171  // otherwise alignment may break on output devices with a different
   172  // physical tab width. docFn should be set to Text or Keyword and will be
   173  // used when converting lbl to a Doc.
   174  func JoinNestedOuter(lbl string, docFn func(string) Doc, d ...Doc) Doc {
   175  	sep := docFn(lbl)
   176  	return &snesting{
   177  		f: func(k int16) Doc {
   178  			if k < int16(len(lbl)+1) {
   179  				// If there is not enough space, don't even try. Just use a
   180  				// regular nested section.
   181  				return JoinNestedRight(sep, d...)
   182  			}
   183  			// If there is enough space, on the other hand, we can de-indent
   184  			// every line after the first.
   185  			return Concat(d[0],
   186  				NestS(int16(-len(lbl)-1),
   187  					FoldMap(
   188  						Concat,
   189  						func(x Doc) Doc {
   190  							return Concat(
   191  								Line,
   192  								ConcatSpace(sep, Align(Group(x))))
   193  						},
   194  						d[1:]...)))
   195  		},
   196  	}
   197  }
   198  
   199  // TableRow is the data for one row of a RLTable (see below).
   200  type TableRow struct {
   201  	Label string
   202  	Doc   Doc
   203  }
   204  
   205  // TableAlignment should be used as first argument to Table().
   206  type TableAlignment int
   207  
   208  const (
   209  	// TableNoAlign does not use alignment and instead uses NestUnder.
   210  	TableNoAlign TableAlignment = iota
   211  	// TableRightAlignFirstColumn right-aligns (left-pads) the first column.
   212  	TableRightAlignFirstColumn
   213  	// TableLeftAlignFirstColumn left-aligns (right-pads) the first column.
   214  	TableLeftAlignFirstColumn
   215  )
   216  
   217  // Table defines a document that formats a list of pairs of items either:
   218  //  - as a 2-column table, with the two columns aligned for example:
   219  //       SELECT aaa
   220  //              bbb
   221  //         FROM ccc
   222  //  - as sections, for example:
   223  //       SELECT
   224  //           aaa
   225  //           bbb
   226  //       FROM
   227  //           ccc
   228  //
   229  // We restrict the left value in each list item to be a one-line string
   230  // to make the width computation efficient.
   231  //
   232  // For convenience, the function also skips over rows with a nil
   233  // pointer as doc.
   234  //
   235  // docFn should be set to Text or Keyword and will be used when converting
   236  // TableRow label's to Docs.
   237  func Table(alignment TableAlignment, docFn func(string) Doc, rows ...TableRow) Doc {
   238  	// Compute the nested formatting in "sections". It's simple.
   239  	// Note that we do not use NestUnder() because we are not grouping
   240  	// at this level (the group is done for the final form below).
   241  	items := makeTableNestedSections(docFn, rows)
   242  	nestedSections := Stack(items...)
   243  
   244  	finalDoc := nestedSections
   245  
   246  	if alignment != TableNoAlign {
   247  		items = makeAlignedTableItems(alignment == TableRightAlignFirstColumn /* leftPad */, docFn, rows, items)
   248  		alignedTable := Stack(items...)
   249  		finalDoc = &union{alignedTable, nestedSections}
   250  	}
   251  
   252  	return Group(finalDoc)
   253  }
   254  
   255  func computeLeftColumnWidth(rows []TableRow) int {
   256  	leftwidth := 0
   257  	for _, r := range rows {
   258  		if r.Doc == nil {
   259  			continue
   260  		}
   261  		if leftwidth < len(r.Label) {
   262  			leftwidth = len(r.Label)
   263  		}
   264  	}
   265  	return leftwidth
   266  }
   267  
   268  func makeAlignedTableItems(
   269  	leftPad bool, docFn func(string) Doc, rows []TableRow, items []Doc,
   270  ) []Doc {
   271  	// We'll need the left column width below.
   272  	leftWidth := computeLeftColumnWidth(rows)
   273  	// Now convert the rows.
   274  	items = items[:0]
   275  	for _, r := range rows {
   276  		if r.Doc == nil || (r.Label == "" && r.Doc == Nil) {
   277  			continue
   278  		}
   279  		if r.Label != "" {
   280  			lbl := docFn(r.Label)
   281  			var pleft Doc
   282  			if leftPad {
   283  				pleft = Concat(pad{int16(leftWidth - len(r.Label))}, lbl)
   284  			} else {
   285  				pleft = Concat(lbl, pad{int16(leftWidth - len(r.Label))})
   286  			}
   287  			d := simplifyNil(pleft, r.Doc,
   288  				func(a, b Doc) Doc {
   289  					return ConcatSpace(a, Align(Group(b)))
   290  				})
   291  			items = append(items, d)
   292  		} else {
   293  			items = append(items, Concat(pad{int16(leftWidth + 1)}, Align(Group(r.Doc))))
   294  		}
   295  	}
   296  	return items
   297  }
   298  
   299  func makeTableNestedSections(docFn func(string) Doc, rows []TableRow) []Doc {
   300  	items := make([]Doc, 0, len(rows))
   301  	for _, r := range rows {
   302  		if r.Doc == nil || (r.Label == "" && r.Doc == Nil) {
   303  			continue
   304  		}
   305  		if r.Label != "" {
   306  			d := simplifyNil(docFn(r.Label), r.Doc,
   307  				func(a, b Doc) Doc {
   308  					return Concat(a, NestT(Concat(Line, Group(b))))
   309  				})
   310  			items = append(items, d)
   311  		} else {
   312  			items = append(items, Group(r.Doc))
   313  		}
   314  	}
   315  	return items
   316  }