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 }