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 }