github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/ui/text.go (about)

     1  package ui
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"math/big"
     7  	"strconv"
     8  
     9  	"github.com/markusbkk/elvish/pkg/eval/vals"
    10  	"github.com/markusbkk/elvish/pkg/wcwidth"
    11  )
    12  
    13  // Text contains of a list of styled Segments.
    14  type Text []*Segment
    15  
    16  // T constructs a new Text with the given content and the given Styling's
    17  // applied.
    18  func T(s string, ts ...Styling) Text {
    19  	return StyleText(Text{&Segment{Text: s}}, ts...)
    20  }
    21  
    22  // Concat concatenates multiple Text's into one.
    23  func Concat(texts ...Text) Text {
    24  	var ret Text
    25  	for _, text := range texts {
    26  		ret = append(ret, text...)
    27  	}
    28  	return ret
    29  }
    30  
    31  // Kind returns "styled-text".
    32  func (Text) Kind() string { return "ui:text" }
    33  
    34  // Repr returns the representation of the current Text. It is just a wrapper
    35  // around the containing Segments.
    36  func (t Text) Repr(indent int) string {
    37  	buf := new(bytes.Buffer)
    38  	for _, s := range t {
    39  		buf.WriteString(s.Repr(indent + 1))
    40  	}
    41  	return fmt.Sprintf("(ui:text %s)", buf.String())
    42  }
    43  
    44  // IterateKeys feeds the function with all valid indices of the styled-text.
    45  func (t Text) IterateKeys(fn func(interface{}) bool) {
    46  	for i := 0; i < len(t); i++ {
    47  		if !fn(strconv.Itoa(i)) {
    48  			break
    49  		}
    50  	}
    51  }
    52  
    53  // Index provides access to the underlying styled-segment.
    54  func (t Text) Index(k interface{}) (interface{}, error) {
    55  	index, err := vals.ConvertListIndex(k, len(t))
    56  	if err != nil {
    57  		return nil, err
    58  	} else if index.Slice {
    59  		return t[index.Lower:index.Upper], nil
    60  	} else {
    61  		return t[index.Lower], nil
    62  	}
    63  }
    64  
    65  // Concat implements Text+string, Text+number, Text+Segment and Text+Text.
    66  func (t Text) Concat(rhs interface{}) (interface{}, error) {
    67  	switch rhs := rhs.(type) {
    68  	case string:
    69  		return Concat(t, T(rhs)), nil
    70  	case int, *big.Int, *big.Rat, float64:
    71  		return Concat(t, T(vals.ToString(rhs))), nil
    72  	case *Segment:
    73  		return Concat(t, Text{rhs}), nil
    74  	case Text:
    75  		return Concat(t, rhs), nil
    76  	}
    77  
    78  	return nil, vals.ErrConcatNotImplemented
    79  }
    80  
    81  // RConcat implements string+Text and number+Text.
    82  func (t Text) RConcat(lhs interface{}) (interface{}, error) {
    83  	switch lhs := lhs.(type) {
    84  	case string:
    85  		return Concat(T(lhs), t), nil
    86  	case int, *big.Int, *big.Rat, float64:
    87  		return Concat(T(vals.ToString(lhs)), t), nil
    88  	}
    89  
    90  	return nil, vals.ErrConcatNotImplemented
    91  }
    92  
    93  // Partition partitions the Text at n indices into n+1 Text values.
    94  func (t Text) Partition(indices ...int) []Text {
    95  	out := make([]Text, len(indices)+1)
    96  	segs := t.Clone()
    97  	for i, idx := range indices {
    98  		toConsume := idx
    99  		if i > 0 {
   100  			toConsume -= indices[i-1]
   101  		}
   102  		for len(segs) > 0 && toConsume > 0 {
   103  			if len(segs[0].Text) <= toConsume {
   104  				out[i] = append(out[i], segs[0])
   105  				toConsume -= len(segs[0].Text)
   106  				segs = segs[1:]
   107  			} else {
   108  				out[i] = append(out[i], &Segment{segs[0].Style, segs[0].Text[:toConsume]})
   109  				segs[0] = &Segment{segs[0].Style, segs[0].Text[toConsume:]}
   110  				toConsume = 0
   111  			}
   112  		}
   113  	}
   114  	if len(segs) > 0 {
   115  		// Don't use segs directly to avoid memory leak
   116  		out[len(indices)] = append(Text(nil), segs...)
   117  	}
   118  	return out
   119  }
   120  
   121  // Clone returns a deep copy of Text.
   122  func (t Text) Clone() Text {
   123  	newt := make(Text, len(t))
   124  	for i, seg := range t {
   125  		newt[i] = seg.Clone()
   126  	}
   127  	return newt
   128  }
   129  
   130  // CountRune counts the number of times a rune occurs in a Text.
   131  func (t Text) CountRune(r rune) int {
   132  	n := 0
   133  	for _, seg := range t {
   134  		n += seg.CountRune(r)
   135  	}
   136  	return n
   137  }
   138  
   139  // CountLines counts the number of lines in a Text. It is equal to
   140  // t.CountRune('\n') + 1.
   141  func (t Text) CountLines() int {
   142  	return t.CountRune('\n') + 1
   143  }
   144  
   145  // SplitByRune splits a Text by the given rune.
   146  func (t Text) SplitByRune(r rune) []Text {
   147  	// Call SplitByRune for each constituent Segment, and "paste" the pairs of
   148  	// subsegments across the segment border. For instance, if Text has 3
   149  	// Segments a, b, c that results in a1, a2, a3, b1, b2, c1, then a3 and b1
   150  	// as well as b2 and c1 are pasted together, and the return value is [a1],
   151  	// [a2], [a3, b1], [b2, c1]. Pasting can happen coalesce: for instance, if
   152  	// Text has 3 Segments a, b, c that results in a1, a2, b1, c1, the return
   153  	// value will be [a1], [a2, b1, c1].
   154  	var result []Text
   155  	var paste Text
   156  	for _, seg := range t {
   157  		subSegs := seg.SplitByRune(r)
   158  		if len(subSegs) == 1 {
   159  			// Only one subsegment. Just paste.
   160  			paste = append(paste, subSegs[0])
   161  			continue
   162  		}
   163  		// Paste the previous trailing segments with the first subsegment, and
   164  		// add it as a Text.
   165  		result = append(result, append(paste, subSegs[0]))
   166  		// For the subsegments in the middle, just add then as is.
   167  		for i := 1; i < len(subSegs)-1; i++ {
   168  			result = append(result, Text{subSegs[i]})
   169  		}
   170  		// The last segment becomes the new paste.
   171  		paste = Text{subSegs[len(subSegs)-1]}
   172  	}
   173  	if len(paste) > 0 {
   174  		result = append(result, paste)
   175  	}
   176  	return result
   177  }
   178  
   179  // TrimWcwidth returns the largest prefix of t that does not exceed the given
   180  // visual width.
   181  func (t Text) TrimWcwidth(wmax int) Text {
   182  	var newt Text
   183  	for _, seg := range t {
   184  		w := wcwidth.Of(seg.Text)
   185  		if w >= wmax {
   186  			newt = append(newt,
   187  				&Segment{seg.Style, wcwidth.Trim(seg.Text, wmax)})
   188  			break
   189  		}
   190  		wmax -= w
   191  		newt = append(newt, seg)
   192  	}
   193  	return newt
   194  }
   195  
   196  // String returns a string representation of the styled text. This now always
   197  // assumes VT-style terminal output.
   198  //
   199  // TODO: Make string conversion sensible to environment, e.g. use HTML when
   200  // output is web.
   201  func (t Text) String() string {
   202  	return t.VTString()
   203  }
   204  
   205  // VTString renders the styled text using VT-style escape sequences.
   206  func (t Text) VTString() string {
   207  	var buf bytes.Buffer
   208  	for _, seg := range t {
   209  		buf.WriteString(seg.VTString())
   210  	}
   211  	return buf.String()
   212  }