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 }