github.com/hoop33/elvish@v0.0.0-20160801152013-6d25485beab4/edit/writer.go (about) 1 package edit 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "strings" 8 "unicode" 9 "unicode/utf8" 10 11 "github.com/elves/elvish/sys" 12 ) 13 14 var logWriterDetail = false 15 16 // cell is an indivisible unit on the screen. It is not necessarily 1 column 17 // wide. 18 type cell struct { 19 rune 20 width byte 21 style string 22 } 23 24 // pos is the position within a buffer. 25 type pos struct { 26 line, col int 27 } 28 29 var invalidPos = pos{-1, -1} 30 31 func lineWidth(cs []cell) int { 32 w := 0 33 for _, c := range cs { 34 w += int(c.width) 35 } 36 return w 37 } 38 39 // buffer reflects a continuous range of lines on the terminal. The Unix 40 // terminal API provides only awkward ways of querying the terminal buffer, so 41 // we keep an internal reflection and do one-way synchronizations (buffer -> 42 // terminal, and not the other way around). This requires us to exactly match 43 // the terminal's idea of the width of characters (wcwidth) and where to 44 // insert soft carriage returns, so there could be bugs. 45 type buffer struct { 46 width, col, indent int 47 newlineWhenFull bool 48 cells [][]cell // cells reflect len(cells) lines on the terminal. 49 dot pos // dot is what the user perceives as the cursor. 50 } 51 52 func newBuffer(width int) *buffer { 53 return &buffer{width: width, cells: [][]cell{make([]cell, 0, width)}} 54 } 55 56 func (b *buffer) appendCell(c cell) { 57 n := len(b.cells) 58 b.cells[n-1] = append(b.cells[n-1], c) 59 b.col += int(c.width) 60 } 61 62 func (b *buffer) appendLine() { 63 b.cells = append(b.cells, make([]cell, 0, b.width)) 64 b.col = 0 65 } 66 67 func (b *buffer) newline() { 68 b.appendLine() 69 70 if b.indent > 0 { 71 for i := 0; i < b.indent; i++ { 72 b.appendCell(cell{rune: ' ', width: 1}) 73 } 74 } 75 } 76 77 func (b *buffer) extend(b2 *buffer, moveDot bool) { 78 if b2 != nil && b2.cells != nil { 79 if moveDot { 80 b.dot.line = b2.dot.line + len(b.cells) 81 b.dot.col = b2.dot.col 82 } 83 b.cells = append(b.cells, b2.cells...) 84 b.col = b2.col 85 } 86 } 87 88 func makeSpacing(n int) []cell { 89 s := make([]cell, n) 90 for i := 0; i < n; i++ { 91 s[i].rune = ' ' 92 s[i].width = 1 93 } 94 return s 95 } 96 97 // extendHorizontal extends b horizontally. It pads each line in b to be at 98 // least of width w and appends the corresponding line in b2 to it, making new 99 // lines in b when b2 has more lines than b. 100 func (b *buffer) extendHorizontal(b2 *buffer, w int) { 101 i := 0 102 for ; i < len(b.cells) && i < len(b2.cells); i++ { 103 if w0 := lineWidth(b.cells[i]); w0 < w { 104 b.cells[i] = append(b.cells[i], makeSpacing(w-w0)...) 105 } 106 b.cells[i] = append(b.cells[i], b2.cells[i]...) 107 } 108 for ; i < len(b2.cells); i++ { 109 row := append(makeSpacing(w), b2.cells[i]...) 110 b.cells = append(b.cells, row) 111 } 112 } 113 114 // write appends a single rune to a buffer. 115 func (b *buffer) write(r rune, style string) { 116 if r == '\n' { 117 b.newline() 118 return 119 } else if !unicode.IsPrint(r) { 120 // BUG(xiaq): buffer.write drops unprintable runes silently 121 return 122 } 123 wd := WcWidth(r) 124 c := cell{r, byte(wd), style} 125 126 if b.col+wd > b.width { 127 b.newline() 128 b.appendCell(c) 129 } else { 130 b.appendCell(c) 131 if b.col == b.width && b.newlineWhenFull { 132 b.newline() 133 } 134 } 135 } 136 137 func (b *buffer) writes(s string, style string) { 138 for _, r := range s { 139 b.write(r, style) 140 } 141 } 142 143 func (b *buffer) writePadding(w int, style string) { 144 b.writes(strings.Repeat(" ", w), style) 145 } 146 147 func (b *buffer) line() int { 148 return len(b.cells) - 1 149 } 150 151 func (b *buffer) cursor() pos { 152 return pos{len(b.cells) - 1, b.col} 153 } 154 155 func (b *buffer) trimToLines(low, high int) { 156 for i := 0; i < low; i++ { 157 b.cells[i] = nil 158 } 159 for i := high; i < len(b.cells); i++ { 160 b.cells[i] = nil 161 } 162 b.cells = b.cells[low:high] 163 b.dot.line -= low 164 } 165 166 // writer renders the editor UI. 167 type writer struct { 168 file *os.File 169 oldBuf *buffer 170 } 171 172 func newWriter(f *os.File) *writer { 173 writer := &writer{file: f, oldBuf: &buffer{}} 174 return writer 175 } 176 177 func (w *writer) resetOldBuf() { 178 w.oldBuf = &buffer{} 179 } 180 181 // deltaPos calculates the escape sequence needed to move the cursor from one 182 // position to another. It use relative movements to move to the destination 183 // line and absolute movement to move to the destination column. 184 func deltaPos(from, to pos) []byte { 185 buf := new(bytes.Buffer) 186 if from.line < to.line { 187 // move down 188 fmt.Fprintf(buf, "\033[%dB", to.line-from.line) 189 } else if from.line > to.line { 190 // move up 191 fmt.Fprintf(buf, "\033[%dA", from.line-to.line) 192 } 193 fmt.Fprintf(buf, "\033[%dG", to.col+1) 194 return buf.Bytes() 195 } 196 197 func compareRows(r1, r2 []cell) (bool, int) { 198 for i, c := range r1 { 199 if i >= len(r2) || c != r2[i] { 200 return false, i 201 } 202 } 203 if len(r1) < len(r2) { 204 return false, len(r1) 205 } 206 return true, 0 207 } 208 209 // commitBuffer updates the terminal display to reflect current buffer. 210 // TODO Instead of erasing w.oldBuf entirely and then draw buf, compute a 211 // delta between w.oldBuf and buf 212 func (w *writer) commitBuffer(bufNoti, buf *buffer, fullRefresh bool) error { 213 if buf.width != w.oldBuf.width && w.oldBuf.cells != nil { 214 // Width change, force full refresh 215 w.oldBuf.cells = nil 216 fullRefresh = true 217 } 218 219 bytesBuf := new(bytes.Buffer) 220 221 // Hide cursor. 222 bytesBuf.WriteString("\033[?25l") 223 224 // Rewind cursor 225 if pLine := w.oldBuf.dot.line; pLine > 0 { 226 fmt.Fprintf(bytesBuf, "\033[%dA", pLine) 227 } 228 bytesBuf.WriteString("\r") 229 230 if fullRefresh { 231 // Do an erase. 232 bytesBuf.WriteString("\033[J") 233 } 234 235 // style of last written cell. 236 style := "" 237 238 switchStyle := func(newstyle string) { 239 if newstyle != style { 240 fmt.Fprintf(bytesBuf, "\033[0;%sm", newstyle) 241 style = newstyle 242 } 243 } 244 245 writeCells := func(cs []cell) { 246 for _, c := range cs { 247 if c.width > 0 { 248 switchStyle(c.style) 249 } 250 bytesBuf.WriteString(string(c.rune)) 251 } 252 } 253 254 if bufNoti != nil { 255 if logWriterDetail { 256 Logger.Printf("going to write %d lines of notifications", len(bufNoti.cells)) 257 } 258 259 // Write notifications 260 for _, line := range bufNoti.cells { 261 writeCells(line) 262 switchStyle("") 263 bytesBuf.WriteString("\033[K\n") 264 } 265 // XXX Hacky. 266 if len(w.oldBuf.cells) > 0 { 267 w.oldBuf.cells = w.oldBuf.cells[1:] 268 } 269 } 270 271 if logWriterDetail { 272 Logger.Printf("going to write %d lines, oldBuf had %d", len(buf.cells), len(w.oldBuf.cells)) 273 } 274 275 for i, line := range buf.cells { 276 if i > 0 { 277 bytesBuf.WriteString("\n") 278 } 279 var j int // First column where buf and oldBuf differ 280 // No need to update current line 281 if !fullRefresh && i < len(w.oldBuf.cells) { 282 var eq bool 283 if eq, j = compareRows(line, w.oldBuf.cells[i]); eq { 284 continue 285 } 286 } 287 // Move to the first differing column if necessary. 288 firstCol := widthOfCells(line[:j]) 289 if firstCol != 0 { 290 fmt.Fprintf(bytesBuf, "\033[%dG", firstCol+1) 291 } 292 // Erase the rest of the line if necessary. 293 if !fullRefresh && i < len(w.oldBuf.cells) && j < len(w.oldBuf.cells[i]) { 294 switchStyle("") 295 bytesBuf.WriteString("\033[K") 296 } 297 writeCells(line[j:]) 298 } 299 if len(w.oldBuf.cells) > len(buf.cells) && !fullRefresh { 300 // If the old buffer is higher, erase old content. 301 // Note that we cannot simply write \033[J, because if the cursor is 302 // just over the last column -- which is precisely the case if we have a 303 // rprompt, \033[J will also erase the last column. 304 switchStyle("") 305 bytesBuf.WriteString("\n\033[J\033[A") 306 } 307 switchStyle("") 308 cursor := buf.cursor() 309 bytesBuf.Write(deltaPos(cursor, buf.dot)) 310 311 // Show cursor. 312 bytesBuf.WriteString("\033[?25h") 313 314 if logWriterDetail { 315 Logger.Printf("going to write %q", bytesBuf.String()) 316 } 317 318 fd := int(w.file.Fd()) 319 if nonblock, _ := sys.GetNonblock(fd); nonblock { 320 sys.SetNonblock(fd, false) 321 defer sys.SetNonblock(fd, true) 322 } 323 324 _, err := w.file.Write(bytesBuf.Bytes()) 325 if err != nil { 326 return err 327 } 328 329 w.oldBuf = buf 330 return nil 331 } 332 333 func widthOfCells(cells []cell) int { 334 w := 0 335 for _, c := range cells { 336 w += int(c.width) 337 } 338 return w 339 } 340 341 func lines(bufs ...*buffer) (l int) { 342 for _, buf := range bufs { 343 if buf != nil { 344 l += len(buf.cells) 345 } 346 } 347 return 348 } 349 350 // findWindow finds a window of lines around the selected line in a total 351 // number of height lines, that is at most max lines. 352 func findWindow(height, selected, max int) (low, high int) { 353 if height <= max { 354 // No need for windowing 355 return 0, height 356 } 357 low = selected - max/2 358 high = low + max 359 switch { 360 case low < 0: 361 // Near top of the list, move the window down 362 low = 0 363 high = low + max 364 case high > height: 365 // Near bottom of the list, move the window down 366 high = height 367 low = high - max 368 } 369 return 370 } 371 372 func trimToWindow(s []string, selected, max int) ([]string, int) { 373 low, high := findWindow(len(s), selected, max) 374 return s[low:high], low 375 } 376 377 func makeModeLine(text string, width int) *buffer { 378 b := newBuffer(width) 379 b.writes(TrimWcWidth(text, width), styleForMode) 380 b.dot = b.cursor() 381 return b 382 } 383 384 // refresh redraws the line editor. The dot is passed as an index into text; 385 // the corresponding position will be calculated. 386 func (w *writer) refresh(es *editorState, fullRefresh bool) error { 387 height, width := sys.GetWinsize(int(w.file.Fd())) 388 mode := es.mode.Mode() 389 390 var bufNoti, bufLine, bufMode, bufTips, bufListing, buf *buffer 391 // butNoti 392 if len(es.notifications) > 0 { 393 bufNoti = newBuffer(width) 394 bufNoti.writes(strings.Join(es.notifications, "\n"), "") 395 es.notifications = nil 396 } 397 398 // bufLine 399 b := newBuffer(width) 400 bufLine = b 401 402 b.newlineWhenFull = true 403 404 b.writes(es.prompt, styleForPrompt) 405 406 if b.line() == 0 && b.col*2 < b.width { 407 b.indent = b.col 408 } 409 410 // i keeps track of number of bytes written. 411 i := 0 412 413 // nowAt is called at every rune boundary. 414 nowAt := func(i int) { 415 if mode == modeCompletion && i == es.completion.begin { 416 c := es.completion.selectedCandidate() 417 b.writes(c.text, styleForCompleted) 418 } 419 if i == es.dot { 420 b.dot = b.cursor() 421 } 422 } 423 nowAt(0) 424 tokens: 425 for _, token := range es.tokens { 426 for _, r := range token.Text { 427 if mode == modeCompletion && 428 es.completion.begin <= i && i <= es.completion.end { 429 // Do nothing. This part is replaced by the completion candidate. 430 } else { 431 b.write(r, styleForType[token.Type]+token.MoreStyle) 432 } 433 i += utf8.RuneLen(r) 434 435 nowAt(i) 436 if mode == modeHistory && i == len(es.hist.prefix) { 437 break tokens 438 } 439 } 440 } 441 442 if mode == modeHistory { 443 // Put the rest of current history, position the cursor at the 444 // end of the line, and finish writing 445 h := es.hist 446 b.writes(h.line[len(h.prefix):], styleForCompletedHistory) 447 b.dot = b.cursor() 448 } 449 450 // Write rprompt 451 padding := b.width - b.col - WcWidths(es.rprompt) 452 if padding >= 1 { 453 b.newlineWhenFull = false 454 b.writePadding(padding, "") 455 b.writes(es.rprompt, styleForRPrompt) 456 } 457 458 // bufMode 459 bufMode = es.mode.ModeLine(width) 460 461 // bufTips 462 // TODO tips is assumed to contain no newlines. 463 if len(es.tips) > 0 { 464 bufTips = newBuffer(width) 465 bufTips.writes(strings.Join(es.tips, "\n"), styleForTip) 466 } 467 468 hListing := 0 469 // Trim lines and determine the maximum height for bufListing 470 // TODO come up with a UI to tell the user that something is not shown. 471 switch { 472 case height >= lines(bufNoti, bufLine, bufMode, bufTips): 473 hListing = height - lines(bufLine, bufMode, bufTips) 474 case height >= lines(bufNoti, bufLine, bufTips): 475 bufMode = nil 476 case height >= lines(bufNoti, bufLine): 477 bufMode = nil 478 if bufTips != nil { 479 bufTips.trimToLines(0, height-lines(bufNoti, bufLine)) 480 } 481 case height >= lines(bufLine): 482 bufTips, bufMode = nil, nil 483 if bufNoti != nil { 484 n := len(bufNoti.cells) 485 bufNoti.trimToLines(n-(height-lines(bufLine)), n) 486 } 487 case height >= 1: 488 bufNoti, bufTips, bufMode = nil, nil, nil 489 dotLine := bufLine.dot.line 490 bufLine.trimToLines(dotLine+1-height, dotLine+1) 491 default: 492 // Broken terminal. Still try to render one line of bufLine. 493 bufNoti, bufTips, bufMode = nil, nil, nil 494 dotLine := bufLine.dot.line 495 bufLine.trimToLines(dotLine, dotLine+1) 496 } 497 498 // bufListing. 499 if hListing > 0 { 500 if lister, ok := es.mode.(Lister); ok { 501 bufListing = lister.List(width, hListing) 502 } 503 // XXX When in completion mode, we re-render the mode line, since the 504 // scrollbar in the mode line depends on completion.lastShown which is 505 // only known after the listing has been rendered. Since rendering the 506 // scrollbar never adds additional lines to bufMode, we may do this 507 // without recalculating the layout. 508 if mode == modeCompletion { 509 bufMode = es.mode.ModeLine(width) 510 } 511 } 512 513 if logWriterDetail { 514 Logger.Printf("bufLine %d, bufMode %d, bufTips %d, bufListing %d", 515 lines(bufLine), lines(bufMode), lines(bufTips), lines(bufListing)) 516 } 517 518 // Combine buffers (reusing bufLine) 519 buf = bufLine 520 buf.extend(bufMode, mode == modeLocation || mode == modeHistoryListing || 521 (mode == modeCompletion && es.completion.filtering) || 522 (mode == modeNavigation && es.navigation.filtering)) 523 buf.extend(bufTips, false) 524 buf.extend(bufListing, false) 525 526 return w.commitBuffer(bufNoti, buf, fullRefresh) 527 }