github.com/hoop33/elvish@v0.0.0-20160801152013-6d25485beab4/edit/listing.go (about)

     1  package edit
     2  
     3  import (
     4  	"container/list"
     5  	"strings"
     6  	"unicode/utf8"
     7  )
     8  
     9  // listing implements a listing mode that supports the notion of selecting an
    10  // entry and filtering entries.
    11  type listing struct {
    12  	typ      ModeType
    13  	provider listingProvider
    14  	selected int
    15  	filter   string
    16  	pagesize int
    17  }
    18  
    19  type listingProvider interface {
    20  	Len() int
    21  	Show(i, w int) styled
    22  	Filter(filter string) int
    23  	Accept(i int, ed *Editor)
    24  	ModeTitle(int) string
    25  }
    26  
    27  type Placeholderer interface {
    28  	Placeholder() string
    29  }
    30  
    31  func newListing(t ModeType, p listingProvider) listing {
    32  	l := listing{t, p, 0, "", 0}
    33  	l.changeFilter("")
    34  	return l
    35  }
    36  
    37  func (l *listing) Mode() ModeType {
    38  	return l.typ
    39  }
    40  
    41  func (l *listing) ModeLine(width int) *buffer {
    42  	title := l.provider.ModeTitle(l.selected)
    43  	// TODO keep it one line.
    44  	b := newBuffer(width)
    45  	b.writes(TrimWcWidth(title, width), styleForMode)
    46  	b.writes(" ", "")
    47  	b.writes(l.filter, styleForFilter)
    48  	b.dot = b.cursor()
    49  	return b
    50  }
    51  
    52  func (l *listing) List(width, maxHeight int) *buffer {
    53  	n := l.provider.Len()
    54  	b := newBuffer(width)
    55  	if n == 0 {
    56  		var ph string
    57  		if pher, ok := l.provider.(Placeholderer); ok {
    58  			ph = pher.Placeholder()
    59  		} else {
    60  			ph = "(no result)"
    61  		}
    62  		b.writes(TrimWcWidth(ph, width), "")
    63  		return b
    64  	}
    65  
    66  	// Collect the entries to show. We start from the selected entry and extend
    67  	// in both directions alternatingly. The entries are collected in a list.
    68  	low := l.selected
    69  	if low == -1 {
    70  		low = 0
    71  	}
    72  	high := low
    73  	height := 0
    74  	var entries list.List
    75  	getEntry := func(i int) (styled, int) {
    76  		s := l.provider.Show(i, width)
    77  		return s, strings.Count(s.text, "\n") + 1
    78  	}
    79  	// We start by extending high, so that the first entry to include is
    80  	// l.selected.
    81  	extendLow := false
    82  	for height < maxHeight && !(low == 0 && high == n) {
    83  		if (extendLow && low > 0) || high == n {
    84  			low--
    85  
    86  			s, h := getEntry(low)
    87  			height += h
    88  			if height > maxHeight {
    89  				// Elide leading lines.
    90  				lines := strings.Split(s.text, "\n")
    91  				s.text = strings.Join(lines[height-maxHeight:], "\n")
    92  				height = maxHeight
    93  			}
    94  			entries.PushFront(s)
    95  		} else {
    96  			s, h := getEntry(high)
    97  			height += h
    98  			if height > maxHeight {
    99  				// Elide trailing lines.
   100  				lines := strings.Split(s.text, "\n")
   101  				s.text = strings.Join(lines[:len(lines)-(height-maxHeight)], "\n")
   102  				height = maxHeight
   103  			}
   104  			entries.PushBack(s)
   105  
   106  			high++
   107  		}
   108  		extendLow = !extendLow
   109  	}
   110  
   111  	l.pagesize = high - low
   112  
   113  	var scrollbar *buffer
   114  	if low > 0 || high < n-1 {
   115  		scrollbar = renderScrollbar(n, low, high, height)
   116  		width--
   117  	}
   118  
   119  	p := entries.Front()
   120  	for i := low; i < high; i++ {
   121  		if i > low {
   122  			b.newline()
   123  		}
   124  		s := p.Value.(styled)
   125  		if i == l.selected {
   126  			s.addStyle(styleForSelected)
   127  		}
   128  		b.writes(s.text, s.style)
   129  		p = p.Next()
   130  	}
   131  	if scrollbar != nil {
   132  		b.extendHorizontal(scrollbar, width)
   133  	}
   134  	return b
   135  }
   136  
   137  func writeHorizontalScrollbar(b *buffer, n, low, high, width int) {
   138  	slow, shigh := findScrollInterval(n, low, high, width)
   139  	for i := 0; i < width; i++ {
   140  		if slow <= i && i < shigh {
   141  			b.write('▉', styleForScrollBar)
   142  		} else {
   143  			b.write('━', styleForScrollBar)
   144  		}
   145  	}
   146  }
   147  
   148  func renderScrollbar(n, low, high, height int) *buffer {
   149  	slow, shigh := findScrollInterval(n, low, high, height)
   150  	// Logger.Printf("low = %d, high = %d, n = %d, slow = %d, shigh = %d", low, high, n, slow, shigh)
   151  	b := newBuffer(1)
   152  	for i := 0; i < height; i++ {
   153  		if i > 0 {
   154  			b.newline()
   155  		}
   156  		if slow <= i && i < shigh {
   157  			b.write('▉', styleForScrollBar)
   158  		} else {
   159  			b.write('│', styleForScrollBar)
   160  		}
   161  	}
   162  	return b
   163  }
   164  
   165  func findScrollInterval(n, low, high, height int) (int, int) {
   166  	f := func(i int) int {
   167  		return int(float64(i)/float64(n)*float64(height) + 0.5)
   168  	}
   169  	scrollLow, scrollHigh := f(low), f(high)
   170  	if scrollLow == scrollHigh {
   171  		if scrollHigh == high {
   172  			scrollLow--
   173  		} else {
   174  			scrollHigh++
   175  		}
   176  	}
   177  	return scrollLow, scrollHigh
   178  }
   179  
   180  func (l *listing) changeFilter(newfilter string) {
   181  	l.filter = newfilter
   182  	l.selected = l.provider.Filter(newfilter)
   183  }
   184  
   185  func (l *listing) backspace() bool {
   186  	_, size := utf8.DecodeLastRuneInString(l.filter)
   187  	if size > 0 {
   188  		l.changeFilter(l.filter[:len(l.filter)-size])
   189  		return true
   190  	}
   191  	return false
   192  }
   193  
   194  func (l *listing) up(cycle bool) {
   195  	n := l.provider.Len()
   196  	if n == 0 {
   197  		return
   198  	}
   199  	l.selected--
   200  	if l.selected == -1 {
   201  		if cycle {
   202  			l.selected += n
   203  		} else {
   204  			l.selected++
   205  		}
   206  	}
   207  }
   208  
   209  func (l *listing) pageUp() {
   210  	n := l.provider.Len()
   211  	if n == 0 {
   212  		return
   213  	}
   214  	l.selected -= l.pagesize
   215  	if l.selected < 0 {
   216  		l.selected = 0
   217  	}
   218  }
   219  
   220  func (l *listing) down(cycle bool) {
   221  	n := l.provider.Len()
   222  	if n == 0 {
   223  		return
   224  	}
   225  	l.selected++
   226  	if l.selected == n {
   227  		if cycle {
   228  			l.selected -= n
   229  		} else {
   230  			l.selected--
   231  		}
   232  	}
   233  }
   234  
   235  func (l *listing) pageDown() {
   236  	n := l.provider.Len()
   237  	if n == 0 {
   238  		return
   239  	}
   240  	l.selected += l.pagesize
   241  	if l.selected >= n {
   242  		l.selected = n - 1
   243  	}
   244  }
   245  
   246  func (l *listing) accept(ed *Editor) {
   247  	if l.selected >= 0 {
   248  		l.provider.Accept(l.selected, ed)
   249  	}
   250  }
   251  
   252  func (l *listing) handleFilterKey(k Key) bool {
   253  	if likeChar(k) {
   254  		l.changeFilter(l.filter + string(k.Rune))
   255  		return true
   256  	}
   257  	return false
   258  }
   259  
   260  func (l *listing) defaultBinding(ed *Editor) {
   261  	if !l.handleFilterKey(ed.lastKey) {
   262  		startInsert(ed)
   263  		ed.nextAction = action{typ: reprocessKey}
   264  	}
   265  }
   266  
   267  func addListingBuiltins(prefix string, l func(*Editor) *listing) {
   268  	add := func(name string, f func(*Editor)) {
   269  		builtins = append(builtins, Builtin{prefix + name, f})
   270  	}
   271  	add("up", func(ed *Editor) { l(ed).up(false) })
   272  	add("up-cycle", func(ed *Editor) { l(ed).up(true) })
   273  	add("page-up", func(ed *Editor) { l(ed).pageUp() })
   274  	add("down", func(ed *Editor) { l(ed).down(false) })
   275  	add("down-cycle", func(ed *Editor) { l(ed).down(true) })
   276  	add("page-down", func(ed *Editor) { l(ed).pageDown() })
   277  	add("backspace", func(ed *Editor) { l(ed).backspace() })
   278  	add("accept", func(ed *Editor) { l(ed).accept(ed) })
   279  	add("default", func(ed *Editor) { l(ed).defaultBinding(ed) })
   280  }
   281  
   282  func addListingDefaultBindings(prefix string, m ModeType) {
   283  	add := func(k Key, name string) {
   284  		if _, ok := defaultBindings[m][k]; !ok {
   285  			defaultBindings[m][k] = prefix + name
   286  		}
   287  	}
   288  	add(Key{Up, 0}, "up")
   289  	add(Key{PageUp, 0}, "page-up")
   290  	add(Key{Down, 0}, "down")
   291  	add(Key{PageDown, 0}, "page-down")
   292  	add(Key{Tab, 0}, "down-cycle")
   293  	add(Key{Backspace, 0}, "backspace")
   294  	add(Key{Enter, 0}, "accept")
   295  	add(Default, "default")
   296  	defaultBindings[m][Key{'[', Ctrl}] = "start-insert"
   297  }