github.com/oweisse/u-root@v0.0.0-20181109060735-d005ad25fef1/cmds/elvish/edit/render.go (about)

     1  package edit
     2  
     3  import (
     4  	"strings"
     5  	"unicode/utf8"
     6  
     7  	"github.com/u-root/u-root/cmds/elvish/edit/highlight"
     8  	"github.com/u-root/u-root/cmds/elvish/edit/ui"
     9  	"github.com/u-root/u-root/cmds/elvish/util"
    10  )
    11  
    12  type placeholderRenderer string
    13  
    14  func (lp placeholderRenderer) Render(b *ui.Buffer) {
    15  	b.WriteString(util.TrimWcwidth(string(lp), b.Width), "")
    16  }
    17  
    18  type listingRenderer struct {
    19  	lines []ui.Styled
    20  }
    21  
    22  func (ls listingRenderer) Render(b *ui.Buffer) {
    23  	for i, line := range ls.lines {
    24  		if i > 0 {
    25  			b.Newline()
    26  		}
    27  		b.WriteString(util.ForceWcwidth(line.Text, b.Width), line.Styles.String())
    28  	}
    29  }
    30  
    31  type listingWithScrollBarRenderer struct {
    32  	listingRenderer
    33  	n, low, high, height int
    34  }
    35  
    36  func (ls listingWithScrollBarRenderer) Render(b *ui.Buffer) {
    37  	b1 := ui.Render(ls.listingRenderer, b.Width-1)
    38  	b.ExtendRight(b1, 0)
    39  
    40  	scrollbar := renderScrollbar(ls.n, ls.low, ls.high, ls.height)
    41  	b.ExtendRight(scrollbar, b.Width-1)
    42  }
    43  
    44  type navRenderer struct {
    45  	maxHeight                      int
    46  	fwParent, fwCurrent, fwPreview int
    47  	parent, current, preview       ui.Renderer
    48  }
    49  
    50  func makeNavRenderer(h int, w1, w2, w3 int, r1, r2, r3 ui.Renderer) ui.Renderer {
    51  	return &navRenderer{h, w1, w2, w3, r1, r2, r3}
    52  }
    53  
    54  const navColMargin = 1
    55  
    56  func (nr *navRenderer) Render(b *ui.Buffer) {
    57  	wParent, wCurrent, wPreview := getNavWidths(b.Width-navColMargin*2,
    58  		nr.fwCurrent, nr.fwPreview)
    59  
    60  	bParent := ui.Render(nr.parent, wParent)
    61  	b.ExtendRight(bParent, 0)
    62  
    63  	bCurrent := ui.Render(nr.current, wCurrent)
    64  	b.ExtendRight(bCurrent, wParent+navColMargin)
    65  
    66  	if wPreview > 0 {
    67  		bPreview := ui.Render(nr.preview, wPreview)
    68  		b.ExtendRight(bPreview, wParent+wCurrent+2*navColMargin)
    69  	}
    70  }
    71  
    72  // linesRenderer renders lines with a uniform style.
    73  type linesRenderer struct {
    74  	lines []string
    75  	style string
    76  }
    77  
    78  func (nr linesRenderer) Render(b *ui.Buffer) {
    79  	b.WriteString(strings.Join(nr.lines, "\n"), "")
    80  }
    81  
    82  // cmdlineRenderer renders the command line, including the prompt, the user's
    83  // input and the rprompt.
    84  type cmdlineRenderer struct {
    85  	prompt  []*ui.Styled
    86  	line    string
    87  	styling *highlight.Styling
    88  	dot     int
    89  	rprompt []*ui.Styled
    90  
    91  	hasRepl   bool
    92  	replBegin int
    93  	replEnd   int
    94  	replText  string
    95  }
    96  
    97  func newCmdlineRenderer(p []*ui.Styled, l string, s *highlight.Styling, d int, rp []*ui.Styled) *cmdlineRenderer {
    98  	return &cmdlineRenderer{prompt: p, line: l, styling: s, dot: d, rprompt: rp}
    99  }
   100  
   101  func (clr *cmdlineRenderer) setRepl(b, e int, t string) {
   102  	clr.hasRepl = true
   103  	clr.replBegin, clr.replEnd, clr.replText = b, e, t
   104  }
   105  
   106  func (clr *cmdlineRenderer) Render(b *ui.Buffer) {
   107  	b.EagerWrap = true
   108  
   109  	b.WriteStyleds(clr.prompt)
   110  
   111  	// If the prompt takes less than half of a line, set the indent.
   112  	if len(b.Lines) == 1 && b.Col*2 < b.Width {
   113  		b.Indent = b.Col
   114  	}
   115  
   116  	// i keeps track of number of bytes written.
   117  	i := 0
   118  
   119  	applier := clr.styling.Apply()
   120  
   121  	// nowAt is called at every rune boundary.
   122  	nowAt := func(i int) {
   123  		applier.At(i)
   124  		// Replacement should be written before setting b.Dot. This way, if the
   125  		// replacement starts right at the dot, the cursor is correctly placed
   126  		// after the replacement.
   127  		if clr.hasRepl && i == clr.replBegin {
   128  			b.WriteString(clr.replText, styleForReplacement.String())
   129  		}
   130  		if i == clr.dot {
   131  			b.Dot = b.Cursor()
   132  		}
   133  	}
   134  	nowAt(0)
   135  
   136  	for _, r := range clr.line {
   137  		if clr.hasRepl && clr.replBegin <= i && i < clr.replEnd {
   138  			// Do nothing. This part is replaced by the replacement.
   139  		} else {
   140  			b.Write(r, applier.Get())
   141  		}
   142  		i += utf8.RuneLen(r)
   143  
   144  		nowAt(i)
   145  	}
   146  
   147  	// Write rprompt
   148  	if len(clr.rprompt) > 0 {
   149  		padding := b.Width - b.Col
   150  		for _, s := range clr.rprompt {
   151  			padding -= util.Wcswidth(s.Text)
   152  		}
   153  		if padding >= 1 {
   154  			b.EagerWrap = false
   155  			b.WriteSpaces(padding, "")
   156  			b.WriteStyleds(clr.rprompt)
   157  		}
   158  	}
   159  }
   160  
   161  var logEditorRender = false
   162  
   163  // editorRenderer renders the entire editor.
   164  type editorRenderer struct {
   165  	*editorState
   166  	height  int
   167  	bufNoti *ui.Buffer
   168  }
   169  
   170  func (er *editorRenderer) Render(buf *ui.Buffer) {
   171  	height, width, es := er.height, buf.Width, er.editorState
   172  
   173  	var bufNoti, bufLine, bufMode, bufTips, bufListing *ui.Buffer
   174  	// butNoti
   175  	if len(es.notifications) > 0 {
   176  		bufNoti = ui.Render(linesRenderer{es.notifications, ""}, width)
   177  		es.notifications = nil
   178  	}
   179  
   180  	// bufLine
   181  	clr := newCmdlineRenderer(es.promptContent, es.buffer, es.styling, es.dot, es.rpromptContent)
   182  	if repl, ok := es.mode.(replacementer); ok {
   183  		clr.setRepl(repl.Replacement())
   184  	}
   185  	bufLine = ui.Render(clr, width)
   186  
   187  	// bufMode
   188  	bufMode = ui.Render(es.mode.ModeLine(), width)
   189  
   190  	// bufTips
   191  	// TODO tips is assumed to contain no newlines.
   192  	if len(es.tips) > 0 {
   193  		bufTips = ui.Render(linesRenderer{es.tips, styleForTip.String()}, width)
   194  	}
   195  
   196  	hListing := 0
   197  	// Trim lines and determine the maximum height for bufListing
   198  	// TODO come up with a UI to tell the user that something is not shown.
   199  	switch {
   200  	case height >= ui.BuffersHeight(bufNoti, bufLine, bufMode, bufTips):
   201  		hListing = height - ui.BuffersHeight(bufLine, bufMode, bufTips)
   202  	case height >= ui.BuffersHeight(bufNoti, bufLine, bufTips):
   203  		bufMode = nil
   204  	case height >= ui.BuffersHeight(bufNoti, bufLine):
   205  		bufMode = nil
   206  		if bufTips != nil {
   207  			bufTips.TrimToLines(0, height-ui.BuffersHeight(bufNoti, bufLine))
   208  		}
   209  	case height >= ui.BuffersHeight(bufLine):
   210  		bufTips, bufMode = nil, nil
   211  		if bufNoti != nil {
   212  			n := len(bufNoti.Lines)
   213  			bufNoti.TrimToLines(n-(height-ui.BuffersHeight(bufLine)), n)
   214  		}
   215  	case height >= 1:
   216  		bufNoti, bufTips, bufMode = nil, nil, nil
   217  		// Determine a window of bufLine that has $height lines around the line
   218  		// where the dot is currently on.
   219  		low := bufLine.Dot.Line - height/2
   220  		high := low + height
   221  		if low < 0 {
   222  			low = 0
   223  			high = low + height
   224  		} else if high > len(bufLine.Lines) {
   225  			high = len(bufLine.Lines)
   226  			low = high - height
   227  		}
   228  		bufLine.TrimToLines(low, high)
   229  	default:
   230  		// Broken terminal. Still try to render one line of bufLine.
   231  		bufNoti, bufTips, bufMode = nil, nil, nil
   232  		dotLine := bufLine.Dot.Line
   233  		bufLine.TrimToLines(dotLine, dotLine+1)
   234  	}
   235  
   236  	// bufListing.
   237  	if hListing > 0 {
   238  		switch mode := es.mode.(type) {
   239  		case listRenderer:
   240  			bufListing = mode.ListRender(width, hListing)
   241  		case lister:
   242  			bufListing = ui.Render(mode.List(hListing), width)
   243  		}
   244  		// XXX When in completion mode, we re-render the mode line, since the
   245  		// scrollbar in the mode line depends on completion.lastShown which is
   246  		// only known after the listing has been rendered. Since rendering the
   247  		// scrollbar never adds additional lines to bufMode, we may do this
   248  		// without recalculating the layout.
   249  		if _, ok := es.mode.(redrawModeLiner); ok {
   250  			bufMode = ui.Render(es.mode.ModeLine(), width)
   251  		}
   252  	}
   253  
   254  	if logEditorRender {
   255  		logger.Printf("bufLine %d, bufMode %d, bufTips %d, bufListing %d",
   256  			ui.BuffersHeight(bufLine), ui.BuffersHeight(bufMode), ui.BuffersHeight(bufTips), ui.BuffersHeight(bufListing))
   257  	}
   258  
   259  	// XXX
   260  	buf.Lines = nil
   261  	// Combine buffers (reusing bufLine)
   262  	buf.Extend(bufLine, true)
   263  	cursorOnModeLine := false
   264  	if coml, ok := es.mode.(cursorOnModeLiner); ok {
   265  		cursorOnModeLine = coml.CursorOnModeLine()
   266  	}
   267  	buf.Extend(bufMode, cursorOnModeLine)
   268  	buf.Extend(bufTips, false)
   269  	buf.Extend(bufListing, false)
   270  
   271  	er.bufNoti = bufNoti
   272  }