github.com/u-root/u-root@v7.0.1-0.20200915234505-ad7babab0a8e+incompatible/cmds/core/elvish/edit/narrow.go (about) 1 package edit 2 3 import ( 4 "container/list" 5 "strings" 6 "unicode/utf8" 7 8 "github.com/u-root/u-root/cmds/core/elvish/edit/eddefs" 9 "github.com/u-root/u-root/cmds/core/elvish/edit/ui" 10 "github.com/u-root/u-root/cmds/core/elvish/eval" 11 "github.com/u-root/u-root/cmds/core/elvish/eval/vals" 12 "github.com/u-root/u-root/cmds/core/elvish/eval/vars" 13 "github.com/u-root/u-root/cmds/core/elvish/hashmap" 14 "github.com/u-root/u-root/cmds/core/elvish/parse/parseutil" 15 ) 16 17 // narrow implements a listing mode that supports the notion of selecting an 18 // entry and filtering entries. 19 type narrow struct { 20 binding eddefs.BindingMap 21 narrowState 22 } 23 24 func init() { atEditorInit(initNarrow) } 25 26 func initNarrow(ed *editor, ns eval.Ns) { 27 n := &narrow{binding: emptyBindingMap} 28 subns := eval.Ns{ 29 "binding": vars.FromPtr(&n.binding), 30 } 31 subns.AddBuiltinFns("edit:narrow:", map[string]interface{}{ 32 "up": func() { n.up(false) }, 33 "up-cycle": func() { n.up(true) }, 34 "page-up": func() { n.pageUp() }, 35 "down": func() { n.down(false) }, 36 "down-cycle": func() { n.down(true) }, 37 "page-down": func() { n.pageDown() }, 38 "backspace": func() { n.backspace() }, 39 "accept": func() { n.accept(ed) }, 40 "accept-close": func() { 41 n.accept(ed) 42 ed.SetModeInsert() 43 }, 44 "toggle-ignore-duplication": func() { 45 n.opts.IgnoreDuplication = !n.opts.IgnoreDuplication 46 n.refresh() 47 }, 48 "toggle-ignore-case": func() { 49 n.opts.IgnoreCase = !n.opts.IgnoreCase 50 n.refresh() 51 }, 52 "default": func() { n.defaultBinding(ed) }, 53 }) 54 ns.AddNs("narrow", subns) 55 ns.AddBuiltinFn("edit:", "-narrow-read", n.NarrowRead) 56 } 57 58 type narrowState struct { 59 name string 60 selected int 61 filter string 62 pagesize int 63 headerWidth int 64 65 placehold string 66 source func() []narrowItem 67 action func(*editor, narrowItem) 68 match func(string, string) bool 69 filtered []narrowItem 70 opts narrowOptions 71 } 72 73 func (l *narrow) Teardown() { 74 l.narrowState = narrowState{} 75 } 76 77 func (l *narrow) Binding(k ui.Key) eval.Callable { 78 if l.opts.bindingMap != nil { 79 if f, ok := l.opts.bindingMap[k]; ok { 80 return f 81 } 82 } 83 return l.binding.GetOrDefault(k) 84 } 85 86 func (l *narrowState) ModeLine() ui.Renderer { 87 ml := l.opts.Modeline 88 var opt []string 89 if l.opts.AutoCommit { 90 opt = append(opt, "A") 91 } 92 if l.opts.IgnoreCase { 93 opt = append(opt, "C") 94 } 95 if l.opts.IgnoreDuplication { 96 opt = append(opt, "D") 97 } 98 if len(opt) != 0 { 99 ml += "[" + strings.Join(opt, " ") + "]" 100 } 101 return ui.NewModeLineRenderer(ml, l.filter) 102 } 103 104 func (l *narrowState) CursorOnModeLine() bool { 105 return true 106 } 107 108 func (l *narrowState) List(maxHeight int) ui.Renderer { 109 if l.opts.MaxLines > 0 && l.opts.MaxLines < maxHeight { 110 maxHeight = l.opts.MaxLines 111 } 112 113 if l.filtered == nil { 114 l.refresh() 115 } 116 n := len(l.filtered) 117 if n == 0 { 118 return placeholderRenderer(l.placehold) 119 } 120 121 // Collect the entries to show. We start from the selected entry and extend 122 // in both directions alternatingly. The entries are split into lines and 123 // then collected in a list. 124 low := l.selected 125 if low == -1 { 126 low = 0 127 } 128 high := low 129 height := 0 130 var listOfLines list.List 131 getEntry := func(i int) []ui.Styled { 132 display := l.filtered[i].Display() 133 lines := strings.Split(display.Text, "\n") 134 styles := display.Styles 135 if i == l.selected { 136 styles = append(styles, styleForSelected...) 137 } 138 styleds := make([]ui.Styled, len(lines)) 139 for i, line := range lines { 140 styleds[i] = ui.Styled{line, styles} 141 } 142 return styleds 143 } 144 // We start by extending high, so that the first entry to include is 145 // l.selected. 146 extendLow := false 147 lastShownIncomplete := false 148 for height < maxHeight && !(low == 0 && high == n) { 149 var i int 150 if (extendLow && low > 0) || high == n { 151 low-- 152 153 entry := getEntry(low) 154 // Prepend at most the last (height - maxHeight) lines. 155 for i = len(entry) - 1; i >= 0 && height < maxHeight; i-- { 156 listOfLines.PushFront(entry[i]) 157 height++ 158 } 159 if i >= 0 { 160 lastShownIncomplete = true 161 } 162 } else { 163 entry := getEntry(high) 164 // Append at most the first (height - maxHeight) lines. 165 for i = 0; i < len(entry) && height < maxHeight; i++ { 166 listOfLines.PushBack(entry[i]) 167 height++ 168 } 169 if i < len(entry) { 170 lastShownIncomplete = true 171 } 172 173 high++ 174 } 175 extendLow = !extendLow 176 } 177 178 l.pagesize = high - low 179 180 // Convert the List to a slice. 181 lines := make([]ui.Styled, 0, listOfLines.Len()) 182 for p := listOfLines.Front(); p != nil; p = p.Next() { 183 lines = append(lines, p.Value.(ui.Styled)) 184 } 185 186 ls := listingRenderer{lines} 187 if low > 0 || high < n || lastShownIncomplete { 188 // Need scrollbar 189 return listingWithScrollBarRenderer{ls, n, low, high, height} 190 } 191 return ls 192 } 193 194 func (l *narrowState) refresh() { 195 var candidates []narrowItem 196 if l.source != nil { 197 candidates = l.source() 198 } 199 l.filtered = make([]narrowItem, 0, len(candidates)) 200 201 filter := l.filter 202 if l.opts.IgnoreCase { 203 filter = strings.ToLower(filter) 204 } 205 206 set := make(map[string]struct{}) 207 208 for _, item := range candidates { 209 text := item.FilterText() 210 s := text 211 if l.opts.IgnoreCase { 212 s = strings.ToLower(s) 213 } 214 if !l.match(s, filter) { 215 continue 216 } 217 if l.opts.IgnoreDuplication { 218 if _, ok := set[text]; ok { 219 continue 220 } 221 set[text] = struct{}{} 222 } 223 l.filtered = append(l.filtered, item) 224 } 225 226 if l.opts.KeepBottom { 227 l.selected = len(l.filtered) - 1 228 } else { 229 l.selected = 0 230 } 231 } 232 233 func (l *narrowState) changeFilter(newfilter string) { 234 l.filter = newfilter 235 l.refresh() 236 } 237 238 func (l *narrowState) backspace() bool { 239 _, size := utf8.DecodeLastRuneInString(l.filter) 240 if size > 0 { 241 l.changeFilter(l.filter[:len(l.filter)-size]) 242 return true 243 } 244 return false 245 } 246 247 func (l *narrowState) up(cycle bool) { 248 n := len(l.filtered) 249 if n == 0 { 250 return 251 } 252 l.selected-- 253 if l.selected == -1 { 254 if cycle { 255 l.selected += n 256 } else { 257 l.selected++ 258 } 259 } 260 } 261 262 func (l *narrowState) pageUp() { 263 n := len(l.filtered) 264 if n == 0 { 265 return 266 } 267 l.selected -= l.pagesize 268 if l.selected < 0 { 269 l.selected = 0 270 } 271 } 272 273 func (l *narrowState) down(cycle bool) { 274 n := len(l.filtered) 275 if n == 0 { 276 return 277 } 278 l.selected++ 279 if l.selected == n { 280 if cycle { 281 l.selected -= n 282 } else { 283 l.selected-- 284 } 285 } 286 } 287 288 func (l *narrowState) pageDown() { 289 n := len(l.filtered) 290 if n == 0 { 291 return 292 } 293 l.selected += l.pagesize 294 if l.selected >= n { 295 l.selected = n - 1 296 } 297 } 298 299 func (l *narrowState) accept(ed *editor) { 300 if l.selected >= 0 { 301 l.action(ed, l.filtered[l.selected]) 302 } 303 } 304 305 func (l *narrowState) handleFilterKey(ed *editor) bool { 306 k := ed.lastKey 307 if likeChar(k) { 308 l.changeFilter(l.filter + string(k.Rune)) 309 if len(l.filtered) == 1 && l.opts.AutoCommit { 310 l.accept(ed) 311 ed.SetModeInsert() 312 } 313 return true 314 } 315 return false 316 } 317 318 func (l *narrowState) defaultBinding(ed *editor) { 319 if !l.handleFilterKey(ed) { 320 ed.SetModeInsert() 321 ed.SetAction(reprocessKey) 322 } 323 } 324 325 type narrowItem interface { 326 Display() ui.Styled 327 Content() string 328 FilterText() string 329 } 330 331 type narrowOptions struct { 332 AutoCommit bool 333 Bindings hashmap.Map 334 IgnoreDuplication bool 335 IgnoreCase bool 336 KeepBottom bool 337 MaxLines int 338 Modeline string 339 340 bindingMap map[ui.Key]eval.Callable 341 } 342 343 type narrowItemString struct { 344 String string 345 } 346 347 func (s *narrowItemString) Content() string { 348 return s.String 349 } 350 351 func (s *narrowItemString) Display() ui.Styled { 352 return ui.Unstyled(s.String) 353 } 354 355 func (s *narrowItemString) FilterText() string { 356 return s.Content() 357 } 358 359 type narrowItemComplex struct { 360 hashmap.Map 361 } 362 363 func (c *narrowItemComplex) Content() string { 364 if v, ok := c.Map.Index("content"); ok { 365 if s, ok := v.(string); ok { 366 return s 367 } 368 } 369 return "" 370 } 371 372 // TODO: add style 373 func (c *narrowItemComplex) Display() ui.Styled { 374 if v, ok := c.Map.Index("display"); ok { 375 if s, ok := v.(string); ok { 376 return ui.Unstyled(s) 377 } 378 } 379 return ui.Unstyled("") 380 } 381 382 func (c *narrowItemComplex) FilterText() string { 383 if v, ok := c.Map.Index("filter-text"); ok { 384 if s, ok := v.(string); ok { 385 return s 386 } 387 } 388 return c.Content() 389 } 390 391 func (n *narrow) NarrowRead(fm *eval.Frame, opts eval.RawOptions, source, action eval.Callable) { 392 l := &narrowState{ 393 opts: narrowOptions{ 394 Bindings: vals.EmptyMap, 395 }, 396 } 397 398 opts.Scan(&l.opts) 399 400 for it := l.opts.Bindings.Iterator(); it.HasElem(); it.Next() { 401 k, v := it.Elem() 402 key := ui.ToKey(k) 403 val, ok := v.(eval.Callable) 404 if !ok { 405 throwf("should be fn") 406 } 407 if l.opts.bindingMap == nil { 408 l.opts.bindingMap = make(map[ui.Key]eval.Callable) 409 } 410 l.opts.bindingMap[key] = val 411 } 412 413 l.source = narrowGetSource(fm, source) 414 l.action = func(ed *editor, item narrowItem) { 415 ed.CallFn(action, item) 416 } 417 // TODO: user customize varible 418 l.match = strings.Contains 419 420 l.changeFilter("") 421 422 n.narrowState = *l 423 fm.Editor.(*editor).SetMode(n) 424 } 425 426 func narrowGetSource(ec *eval.Frame, source eval.Callable) func() []narrowItem { 427 return func() []narrowItem { 428 ed := ec.Editor.(*editor) 429 vs, err := ec.CaptureOutput(source, eval.NoArgs, eval.NoOpts) 430 if err != nil { 431 ed.Notify(err.Error()) 432 return nil 433 } 434 var lis []narrowItem 435 for _, v := range vs { 436 switch raw := v.(type) { 437 case string: 438 lis = append(lis, &narrowItemString{raw}) 439 case hashmap.Map: 440 lis = append(lis, &narrowItemComplex{raw}) 441 } 442 } 443 return lis 444 } 445 } 446 447 func (ed *editor) replaceInput(text string) { 448 ed.buffer = text 449 } 450 451 func wordifyBuiltin(fm *eval.Frame, text string) { 452 out := fm.OutputChan() 453 for _, s := range parseutil.Wordify(text) { 454 out <- s 455 } 456 }