github.com/xyproto/u-root@v6.0.1-0.20200302025726-5528e0c77a3c+incompatible/cmds/core/elvish/edit/ui/buffer.go (about)

     1  package ui
     2  
     3  import (
     4  	"strings"
     5  
     6  	"github.com/u-root/u-root/cmds/core/elvish/util"
     7  )
     8  
     9  // Cell is an indivisible unit on the screen. It is not necessarily 1 column
    10  // wide.
    11  type Cell struct {
    12  	Text  string
    13  	Width byte
    14  	Style string
    15  }
    16  
    17  // Pos is the position within a buffer.
    18  type Pos struct {
    19  	Line, Col int
    20  }
    21  
    22  // CellsWidth returns the total width of a Cell slice.
    23  func CellsWidth(cs []Cell) int {
    24  	w := 0
    25  	for _, c := range cs {
    26  		w += int(c.Width)
    27  	}
    28  	return w
    29  }
    30  
    31  // CompareCells returns whether two Cell slices are equal, and when they are
    32  // not, the first index at which they differ.
    33  func CompareCells(r1, r2 []Cell) (bool, int) {
    34  	for i, c := range r1 {
    35  		if i >= len(r2) || c != r2[i] {
    36  			return false, i
    37  		}
    38  	}
    39  	if len(r1) < len(r2) {
    40  		return false, len(r1)
    41  	}
    42  	return true, 0
    43  }
    44  
    45  // Buffer reflects a continuous range of lines on the terminal.
    46  //
    47  // The Unix terminal API provides only awkward ways of querying the terminal
    48  // Buffer, so we keep an internal reflection and do one-way synchronizations
    49  // (Buffer -> terminal, and not the other way around). This requires us to
    50  // exactly match the terminal's idea of the width of characters (wcwidth) and
    51  // where to insert soft carriage returns, so there could be bugs.
    52  type Buffer struct {
    53  	Width, Col, Indent int
    54  	// EagerWrap controls whether to wrap line as soon as the cursor reaches the
    55  	// right edge of the terminal. This is not often desirable as it creates
    56  	// unneessary line breaks, but is is useful when echoing the user input.
    57  	// will otherwise
    58  	EagerWrap bool
    59  	// Lines the content of the buffer.
    60  	Lines [][]Cell
    61  	// Dot is what the user perceives as the cursor.
    62  	Dot Pos
    63  }
    64  
    65  // NewBuffer builds a new buffer, with one empty line.
    66  func NewBuffer(width int) *Buffer {
    67  	return &Buffer{Width: width, Lines: [][]Cell{make([]Cell, 0, width)}}
    68  }
    69  
    70  func (b *Buffer) SetIndent(indent int) *Buffer {
    71  	b.Indent = indent
    72  	return b
    73  }
    74  
    75  func (b *Buffer) SetEagerWrap(v bool) *Buffer {
    76  	b.EagerWrap = v
    77  	return b
    78  }
    79  
    80  func (b *Buffer) SetLines(lines ...[]Cell) *Buffer {
    81  	b.Lines = lines
    82  	b.Col = CellsWidth(lines[len(lines)-1])
    83  	return b
    84  }
    85  
    86  func (b *Buffer) SetDot(dot Pos) *Buffer {
    87  	b.Dot = dot
    88  	return b
    89  }
    90  
    91  // Cursor returns the current position of the cursor.
    92  func (b *Buffer) Cursor() Pos {
    93  	return Pos{len(b.Lines) - 1, b.Col}
    94  }
    95  
    96  // BuffersHeight computes the combined height of a number of buffers.
    97  func BuffersHeight(bufs ...*Buffer) (l int) {
    98  	for _, buf := range bufs {
    99  		if buf != nil {
   100  			l += len(buf.Lines)
   101  		}
   102  	}
   103  	return
   104  }
   105  
   106  // Low level buffer mutations.
   107  
   108  func (b *Buffer) appendLine() {
   109  	b.Lines = append(b.Lines, make([]Cell, 0, b.Width))
   110  	b.Col = 0
   111  }
   112  
   113  func (b *Buffer) appendCell(c Cell) {
   114  	n := len(b.Lines)
   115  	b.Lines[n-1] = append(b.Lines[n-1], c)
   116  	b.Col += int(c.Width)
   117  }
   118  
   119  // High-level buffer mutations.
   120  
   121  // Newline starts a newline.
   122  func (b *Buffer) Newline() {
   123  	b.appendLine()
   124  
   125  	if b.Indent > 0 {
   126  		for i := 0; i < b.Indent; i++ {
   127  			b.appendCell(Cell{Text: " ", Width: 1})
   128  		}
   129  	}
   130  }
   131  
   132  var styleForControlChar = Styles{"inverse"}
   133  
   134  // Write writes a single rune to a buffer, wrapping the line when needed. If the
   135  // rune is a control character, it will be written using the caret notation
   136  // (like ^X) and gets the additional style of styleForControlChar.
   137  func (b *Buffer) Write(r rune, style string) {
   138  	if r == '\n' {
   139  		b.Newline()
   140  		return
   141  	}
   142  	wd := util.Wcwidth(r)
   143  	c := Cell{string(r), byte(wd), style}
   144  	if r < 0x20 || r == 0x7f {
   145  		wd = 2
   146  		if style != "" {
   147  			style = style + ";" + styleForControlChar.String()
   148  		} else {
   149  			style = styleForControlChar.String()
   150  		}
   151  		c = Cell{"^" + string(r^0x40), 2, style}
   152  	}
   153  
   154  	if b.Col+wd > b.Width {
   155  		b.Newline()
   156  		b.appendCell(c)
   157  	} else {
   158  		b.appendCell(c)
   159  		if b.Col == b.Width && b.EagerWrap {
   160  			b.Newline()
   161  		}
   162  	}
   163  }
   164  
   165  // WriteString writes a string to a buffer, with one style.
   166  func (b *Buffer) WriteString(text, style string) {
   167  	for _, r := range text {
   168  		b.Write(r, style)
   169  	}
   170  }
   171  
   172  // WriteSpaces writes w spaces.
   173  func (b *Buffer) WriteSpaces(w int, style string) {
   174  	b.WriteString(strings.Repeat(" ", w), style)
   175  }
   176  
   177  // WriteStyleds writes a slice of styled structs.
   178  func (b *Buffer) WriteStyleds(ss []*Styled) {
   179  	for _, s := range ss {
   180  		b.WriteString(s.Text, s.Styles.String())
   181  	}
   182  }
   183  
   184  // TrimToLines trims a buffer to the lines [low, high).
   185  func (b *Buffer) TrimToLines(low, high int) {
   186  	for i := 0; i < low; i++ {
   187  		b.Lines[i] = nil
   188  	}
   189  	for i := high; i < len(b.Lines); i++ {
   190  		b.Lines[i] = nil
   191  	}
   192  	b.Lines = b.Lines[low:high]
   193  	b.Dot.Line -= low
   194  	if b.Dot.Line < 0 {
   195  		b.Dot.Line = 0
   196  	}
   197  }
   198  
   199  // Extend adds all lines from b2 to the bottom of this buffer. If moveDot is
   200  // true, the dot is updated to match the dot of b2.
   201  func (b *Buffer) Extend(b2 *Buffer, moveDot bool) {
   202  	if b2 != nil && b2.Lines != nil {
   203  		if moveDot {
   204  			b.Dot.Line = b2.Dot.Line + len(b.Lines)
   205  			b.Dot.Col = b2.Dot.Col
   206  		}
   207  		b.Lines = append(b.Lines, b2.Lines...)
   208  		b.Col = b2.Col
   209  	}
   210  }
   211  
   212  // ExtendRight extends b to the right. It pads each line in b to be at least of
   213  // width w and appends the corresponding line in b2 to it, making new lines in b
   214  // when b2 has more lines than b.
   215  // BUG(xiaq): after calling ExtendRight, the widths of some lines can exceed
   216  // b.width.
   217  func (b *Buffer) ExtendRight(b2 *Buffer, w int) {
   218  	i := 0
   219  	for ; i < len(b.Lines) && i < len(b2.Lines); i++ {
   220  		if w0 := CellsWidth(b.Lines[i]); w0 < w {
   221  			b.Lines[i] = append(b.Lines[i], makeSpacing(w-w0)...)
   222  		}
   223  		b.Lines[i] = append(b.Lines[i], b2.Lines[i]...)
   224  	}
   225  	for ; i < len(b2.Lines); i++ {
   226  		row := append(makeSpacing(w), b2.Lines[i]...)
   227  		b.Lines = append(b.Lines, row)
   228  	}
   229  	b.Col = CellsWidth(b.Lines[len(b.Lines)-1])
   230  }
   231  
   232  func makeSpacing(n int) []Cell {
   233  	s := make([]Cell, n)
   234  	for i := 0; i < n; i++ {
   235  		s[i].Text = " "
   236  		s[i].Width = 1
   237  	}
   238  	return s
   239  }