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  }