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  }