github.com/u-root/u-root@v7.0.1-0.20200915234505-ad7babab0a8e+incompatible/cmds/core/elvish/edit/listing.go (about)

     1  package edit
     2  
     3  import (
     4  	"container/list"
     5  	"fmt"
     6  	"strings"
     7  	"unicode/utf8"
     8  
     9  	"github.com/u-root/u-root/cmds/core/elvish/edit/eddefs"
    10  	"github.com/u-root/u-root/cmds/core/elvish/edit/ui"
    11  	"github.com/u-root/u-root/cmds/core/elvish/eval"
    12  	"github.com/u-root/u-root/cmds/core/elvish/eval/vars"
    13  	"github.com/u-root/u-root/cmds/core/elvish/util"
    14  )
    15  
    16  // listingMode implements a mode that supports listing, selecting and filtering
    17  // entries.
    18  type listingMode struct {
    19  	commonBinding eddefs.BindingMap
    20  	listingState
    21  }
    22  
    23  type listingState struct {
    24  	binding     eddefs.BindingMap
    25  	provider    eddefs.ListingProvider
    26  	selected    int
    27  	filter      string
    28  	pagesize    int
    29  	headerWidth int
    30  }
    31  
    32  func init() { atEditorInit(initListing) }
    33  
    34  func initListing(ed *editor, ns eval.Ns) {
    35  	l := &listingMode{commonBinding: emptyBindingMap}
    36  	ed.listing = l
    37  
    38  	subns := eval.Ns{
    39  		"binding": vars.FromPtr(&l.commonBinding),
    40  	}
    41  	subns.AddBuiltinFns("edit:listing:", map[string]interface{}{
    42  		"up":         func() { l.up(false) },
    43  		"up-cycle":   func() { l.up(true) },
    44  		"page-up":    func() { l.pageUp() },
    45  		"down":       func() { l.down(false) },
    46  		"down-cycle": func() { l.down(true) },
    47  		"page-down":  func() { l.pageDown() },
    48  		"backspace":  func() { l.backspace() },
    49  		"accept":     func() { l.accept(ed) },
    50  		"accept-close": func() {
    51  			l.accept(ed)
    52  			ed.SetModeInsert()
    53  		},
    54  		"default": func() { l.defaultBinding(ed) },
    55  	})
    56  	ns.AddNs("listing", subns)
    57  }
    58  
    59  type placeholderer interface {
    60  	Placeholder() string
    61  }
    62  
    63  func (l *listingMode) Teardown() {
    64  	l.listingState = listingState{}
    65  	if p, ok := l.provider.(teardowner); ok {
    66  		p.Teardown()
    67  	}
    68  }
    69  
    70  type teardowner interface {
    71  	Teardown()
    72  }
    73  
    74  func (l *listingMode) Binding(k ui.Key) eval.Callable {
    75  	specificBindings := l.binding
    76  	listingBindings := l.commonBinding
    77  	// mode-specific binding -> listing binding ->
    78  	// mode-specific default -> listing default
    79  	switch {
    80  	case specificBindings.HasKey(k):
    81  		return specificBindings.GetKey(k)
    82  	case listingBindings.HasKey(k):
    83  		return listingBindings.GetKey(k)
    84  	case specificBindings.HasKey(ui.Default):
    85  		return specificBindings.GetKey(ui.Default)
    86  	case listingBindings.HasKey(ui.Default):
    87  		return listingBindings.GetKey(ui.Default)
    88  	default:
    89  		return nil
    90  	}
    91  }
    92  
    93  func newListing(b eddefs.BindingMap, p eddefs.ListingProvider) *listingState {
    94  	l := &listingState{}
    95  	l.setup(b, p)
    96  	return l
    97  }
    98  
    99  func (l *listingState) setup(b eddefs.BindingMap, p eddefs.ListingProvider) {
   100  	*l = listingState{b, p, 0, "", 0, 0}
   101  	l.refresh()
   102  	for i := 0; i < p.Len(); i++ {
   103  		header, _ := p.Show(i)
   104  		width := util.Wcswidth(header)
   105  		if l.headerWidth < width {
   106  			l.headerWidth = width
   107  		}
   108  	}
   109  }
   110  
   111  func (l *listingState) ModeLine() ui.Renderer {
   112  	return ui.NewModeLineRenderer(l.provider.ModeTitle(l.selected), l.filter)
   113  }
   114  
   115  func (l *listingState) CursorOnModeLine() bool {
   116  	if c, ok := l.provider.(cursorOnModeLiner); ok {
   117  		return c.CursorOnModeLine()
   118  	}
   119  	return false
   120  }
   121  
   122  func (l *listingState) List(maxHeight int) ui.Renderer {
   123  	n := l.provider.Len()
   124  	if n == 0 {
   125  		var ph string
   126  		if pher, ok := l.provider.(placeholderer); ok {
   127  			ph = pher.Placeholder()
   128  		} else {
   129  			ph = "(no result)"
   130  		}
   131  		return placeholderRenderer(ph)
   132  	}
   133  
   134  	// Collect the entries to show. We start from the selected entry and extend
   135  	// in both directions alternatingly. The entries are split into lines and
   136  	// then collected in a list.
   137  	low := l.selected
   138  	if low == -1 {
   139  		low = 0
   140  	}
   141  	high := low
   142  	height := 0
   143  	var listOfLines list.List
   144  	getEntry := func(i int) []ui.Styled {
   145  		header, content := l.provider.Show(i)
   146  		lines := strings.Split(content.Text, "\n")
   147  		styles := content.Styles
   148  		if i == l.selected {
   149  			styles = append(styles, styleForSelected...)
   150  		}
   151  		styleds := make([]ui.Styled, len(lines))
   152  		for i, line := range lines {
   153  			if l.headerWidth > 0 {
   154  				if i == 0 {
   155  					line = fmt.Sprintf("%*s %s", l.headerWidth, header, line)
   156  				} else {
   157  					line = fmt.Sprintf("%*s %s", l.headerWidth, "", line)
   158  				}
   159  			}
   160  			styleds[i] = ui.Styled{line, styles}
   161  		}
   162  		return styleds
   163  	}
   164  	// We start by extending high, so that the first entry to include is
   165  	// l.selected.
   166  	extendLow := false
   167  	lastShownIncomplete := false
   168  	for height < maxHeight && !(low == 0 && high == n) {
   169  		var i int
   170  		if (extendLow && low > 0) || high == n {
   171  			low--
   172  
   173  			entry := getEntry(low)
   174  			// Prepend at most the last (height - maxHeight) lines.
   175  			for i = len(entry) - 1; i >= 0 && height < maxHeight; i-- {
   176  				listOfLines.PushFront(entry[i])
   177  				height++
   178  			}
   179  			if i >= 0 {
   180  				lastShownIncomplete = true
   181  			}
   182  		} else {
   183  			entry := getEntry(high)
   184  			// Append at most the first (height - maxHeight) lines.
   185  			for i = 0; i < len(entry) && height < maxHeight; i++ {
   186  				listOfLines.PushBack(entry[i])
   187  				height++
   188  			}
   189  			if i < len(entry) {
   190  				lastShownIncomplete = true
   191  			}
   192  
   193  			high++
   194  		}
   195  		extendLow = !extendLow
   196  	}
   197  
   198  	l.pagesize = high - low
   199  
   200  	// Convert the List to a slice.
   201  	lines := make([]ui.Styled, 0, listOfLines.Len())
   202  	for p := listOfLines.Front(); p != nil; p = p.Next() {
   203  		lines = append(lines, p.Value.(ui.Styled))
   204  	}
   205  
   206  	ls := listingRenderer{lines}
   207  	if low > 0 || high < n || lastShownIncomplete {
   208  		// Need scrollbar
   209  		return listingWithScrollBarRenderer{ls, n, low, high, height}
   210  	}
   211  	return ls
   212  }
   213  
   214  func writeHorizontalScrollbar(b *ui.Buffer, n, low, high, width int) {
   215  	slow, shigh := findScrollInterval(n, low, high, width)
   216  	for i := 0; i < width; i++ {
   217  		if slow <= i && i < shigh {
   218  			b.Write(' ', styleForScrollBarThumb.String())
   219  		} else {
   220  			b.Write('━', styleForScrollBarArea.String())
   221  		}
   222  	}
   223  }
   224  
   225  func renderScrollbar(n, low, high, height int) *ui.Buffer {
   226  	slow, shigh := findScrollInterval(n, low, high, height)
   227  	// Logger.Printf("low = %d, high = %d, n = %d, slow = %d, shigh = %d", low, high, n, slow, shigh)
   228  	b := ui.NewBuffer(1)
   229  	for i := 0; i < height; i++ {
   230  		if i > 0 {
   231  			b.Newline()
   232  		}
   233  		if slow <= i && i < shigh {
   234  			b.Write(' ', styleForScrollBarThumb.String())
   235  		} else {
   236  			b.Write('│', styleForScrollBarArea.String())
   237  		}
   238  	}
   239  	return b
   240  }
   241  
   242  func findScrollInterval(n, low, high, height int) (int, int) {
   243  	f := func(i int) int {
   244  		return int(float64(i)/float64(n)*float64(height) + 0.5)
   245  	}
   246  	scrollLow, scrollHigh := f(low), f(high)
   247  	if scrollLow == scrollHigh {
   248  		if scrollHigh == high {
   249  			scrollLow--
   250  		} else {
   251  			scrollHigh++
   252  		}
   253  	}
   254  	return scrollLow, scrollHigh
   255  }
   256  
   257  func (l *listingState) changeFilter(newfilter string) {
   258  	l.filter = newfilter
   259  	l.refresh()
   260  }
   261  
   262  func (l *listingState) refresh() {
   263  	l.selected = l.provider.Filter(l.filter)
   264  }
   265  
   266  func (l *listingState) backspace() bool {
   267  	_, size := utf8.DecodeLastRuneInString(l.filter)
   268  	if size > 0 {
   269  		l.changeFilter(l.filter[:len(l.filter)-size])
   270  		return true
   271  	}
   272  	return false
   273  }
   274  
   275  func (l *listingState) up(cycle bool) {
   276  	n := l.provider.Len()
   277  	if n == 0 {
   278  		return
   279  	}
   280  	l.selected--
   281  	if l.selected == -1 {
   282  		if cycle {
   283  			l.selected += n
   284  		} else {
   285  			l.selected++
   286  		}
   287  	}
   288  }
   289  
   290  func (l *listingState) pageUp() {
   291  	n := l.provider.Len()
   292  	if n == 0 {
   293  		return
   294  	}
   295  	l.selected -= l.pagesize
   296  	if l.selected < 0 {
   297  		l.selected = 0
   298  	}
   299  }
   300  
   301  func (l *listingState) down(cycle bool) {
   302  	n := l.provider.Len()
   303  	if n == 0 {
   304  		return
   305  	}
   306  	l.selected++
   307  	if l.selected == n {
   308  		if cycle {
   309  			l.selected -= n
   310  		} else {
   311  			l.selected--
   312  		}
   313  	}
   314  }
   315  
   316  func (l *listingState) pageDown() {
   317  	n := l.provider.Len()
   318  	if n == 0 {
   319  		return
   320  	}
   321  	l.selected += l.pagesize
   322  	if l.selected >= n {
   323  		l.selected = n - 1
   324  	}
   325  }
   326  
   327  func (l *listingState) accept(ed *editor) {
   328  	if l.selected >= 0 {
   329  		l.provider.Accept(l.selected, ed)
   330  	}
   331  }
   332  
   333  func (l *listingState) defaultBinding(ed *editor) {
   334  	k := ed.LastKey()
   335  	if likeChar(k) {
   336  		// Append to filter
   337  		l.changeFilter(l.filter + string(k.Rune))
   338  		if aa, ok := l.provider.(autoAccepter); ok {
   339  			if aa.AutoAccept() {
   340  				l.accept(ed)
   341  			}
   342  		}
   343  	} else {
   344  		ed.SetModeInsert()
   345  		ed.SetAction(reprocessKey)
   346  	}
   347  }
   348  
   349  type autoAccepter interface {
   350  	AutoAccept() bool
   351  }