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 }