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