src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/cli/tk/codearea_render.go (about)

     1  package tk
     2  
     3  import (
     4  	"src.elv.sh/pkg/cli/term"
     5  	"src.elv.sh/pkg/ui"
     6  	"src.elv.sh/pkg/wcwidth"
     7  )
     8  
     9  // View model, calculated from State and used for rendering.
    10  type view struct {
    11  	prompt  ui.Text
    12  	rprompt ui.Text
    13  	code    ui.Text
    14  	dot     int
    15  	tips    []ui.Text
    16  }
    17  
    18  var stylingForPending = ui.Underlined
    19  
    20  func getView(w *codeArea) *view {
    21  	s := w.CopyState()
    22  	code, pFrom, pTo := patchPending(s.Buffer, s.Pending)
    23  	styledCode, errors := w.Highlighter(code.Content)
    24  	if s.HideTips {
    25  		errors = nil
    26  	}
    27  	if pFrom < pTo {
    28  		// Apply stylingForPending to [pFrom, pTo)
    29  		parts := styledCode.Partition(pFrom, pTo)
    30  		pending := ui.StyleText(parts[1], stylingForPending)
    31  		styledCode = ui.Concat(parts[0], pending, parts[2])
    32  	}
    33  
    34  	var rprompt ui.Text
    35  	if !s.HideRPrompt {
    36  		rprompt = w.RPrompt()
    37  	}
    38  
    39  	return &view{w.Prompt(), rprompt, styledCode, code.Dot, errors}
    40  }
    41  
    42  func patchPending(c CodeBuffer, p PendingCode) (CodeBuffer, int, int) {
    43  	if p.From > p.To || p.From < 0 || p.To > len(c.Content) {
    44  		// Invalid Pending.
    45  		return c, 0, 0
    46  	}
    47  	if p.From == p.To && p.Content == "" {
    48  		return c, 0, 0
    49  	}
    50  	newContent := c.Content[:p.From] + p.Content + c.Content[p.To:]
    51  	newDot := 0
    52  	switch {
    53  	case c.Dot < p.From:
    54  		// Dot is before the replaced region. Keep it.
    55  		newDot = c.Dot
    56  	case c.Dot >= p.From && c.Dot < p.To:
    57  		// Dot is within the replaced region. Place the dot at the end.
    58  		newDot = p.From + len(p.Content)
    59  	case c.Dot >= p.To:
    60  		// Dot is after the replaced region. Maintain the relative position of
    61  		// the dot.
    62  		newDot = c.Dot - (p.To - p.From) + len(p.Content)
    63  	}
    64  	return CodeBuffer{Content: newContent, Dot: newDot}, p.From, p.From + len(p.Content)
    65  }
    66  
    67  func renderView(v *view, buf *term.BufferBuilder) {
    68  	buf.EagerWrap = true
    69  
    70  	buf.WriteStyled(v.prompt)
    71  	if len(buf.Lines) == 1 && buf.Col*2 < buf.Width {
    72  		buf.Indent = buf.Col
    73  	}
    74  
    75  	parts := v.code.Partition(v.dot)
    76  	buf.
    77  		WriteStyled(parts[0]).
    78  		SetDotHere().
    79  		WriteStyled(parts[1])
    80  
    81  	buf.EagerWrap = false
    82  	buf.Indent = 0
    83  
    84  	// Handle rprompts with newlines.
    85  	if rpromptWidth := styledWcswidth(v.rprompt); rpromptWidth > 0 {
    86  		padding := buf.Width - buf.Col - rpromptWidth
    87  		if padding >= 1 {
    88  			buf.WriteSpaces(padding)
    89  			buf.WriteStyled(v.rprompt)
    90  		}
    91  	}
    92  
    93  	for _, tip := range v.tips {
    94  		buf.Newline()
    95  		buf.WriteStyled(tip)
    96  	}
    97  }
    98  
    99  func truncateToHeight(b *term.Buffer, maxHeight int) {
   100  	switch {
   101  	case len(b.Lines) <= maxHeight:
   102  		// We can show all line; do nothing.
   103  	case b.Dot.Line < maxHeight:
   104  		// We can show all lines before the cursor, and as many lines after the
   105  		// cursor as we can, adding up to maxHeight.
   106  		b.TrimToLines(0, maxHeight)
   107  	default:
   108  		// We can show maxHeight lines before and including the cursor line.
   109  		b.TrimToLines(b.Dot.Line-maxHeight+1, b.Dot.Line+1)
   110  	}
   111  }
   112  
   113  func styledWcswidth(t ui.Text) int {
   114  	w := 0
   115  	for _, seg := range t {
   116  		w += wcwidth.Of(seg.Text)
   117  	}
   118  	return w
   119  }