github.com/xyproto/u-root@v6.0.1-0.20200302025726-5528e0c77a3c+incompatible/cmds/core/elvish/edit/completion/completion_mode.go (about)

     1  package completion
     2  
     3  import (
     4  	"fmt"
     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  	"github.com/u-root/u-root/cmds/core/elvish/util"
    16  )
    17  
    18  // Completion mode.
    19  
    20  // Interface.
    21  
    22  type completion struct {
    23  	binding      eddefs.BindingMap
    24  	matcher      hashmap.Map
    25  	argCompleter hashmap.Map
    26  	completionState
    27  }
    28  
    29  type completionState struct {
    30  	complSpec
    31  	completer string
    32  
    33  	filtering       bool
    34  	filter          string
    35  	filtered        []*candidate
    36  	selected        int
    37  	firstShown      int
    38  	lastShownInFull int
    39  	height          int
    40  }
    41  
    42  func Init(ed eddefs.Editor, ns eval.Ns) {
    43  	c := &completion{
    44  		binding:      eddefs.EmptyBindingMap,
    45  		matcher:      vals.MakeMapFromKV("", matchPrefix),
    46  		argCompleter: makeArgCompleter(),
    47  	}
    48  
    49  	ns.AddNs("completion",
    50  		eval.Ns{
    51  			"binding":       vars.FromPtr(&c.binding),
    52  			"matcher":       vars.FromPtr(&c.matcher),
    53  			"arg-completer": vars.FromPtr(&c.argCompleter),
    54  		}.AddBuiltinFns("edit:completion:", map[string]interface{}{
    55  			"start":          func() { c.start(ed, false) },
    56  			"smart-start":    func() { c.start(ed, true) },
    57  			"up":             func() { c.prev(false) },
    58  			"up-cycle":       func() { c.prev(true) },
    59  			"down":           func() { c.next(false) },
    60  			"down-cycle":     func() { c.next(true) },
    61  			"left":           c.left,
    62  			"right":          c.right,
    63  			"accept":         func() { c.accept(ed) },
    64  			"trigger-filter": c.triggerFilter,
    65  			"default":        func() { c.complDefault(ed) },
    66  		}))
    67  
    68  	// Exposing arg completers.
    69  	for _, v := range argCompletersData {
    70  		ns[v.name+eval.FnSuffix] = vars.NewRo(
    71  			&builtinArgCompleter{v.name, v.impl, c.argCompleter})
    72  	}
    73  
    74  	// Matchers.
    75  	ns.AddFn("match-prefix", matchPrefix)
    76  	ns.AddFn("match-substr", matchSubstr)
    77  	ns.AddFn("match-subseq", matchSubseq)
    78  
    79  	// Other functions.
    80  	ns.AddBuiltinFn("edit:", "complete-getopt", complGetopt)
    81  	ns.AddBuiltinFnCustom("edit:", "complete-candidate", &makeComplexCandidateCallable{})
    82  }
    83  
    84  type makeComplexCandidateCallable struct {
    85  }
    86  
    87  func (*makeComplexCandidateCallable) Target() interface{} {
    88  	return makeComplexCandidate
    89  }
    90  
    91  func (*makeComplexCandidateCallable) Call(
    92  	f *eval.Frame, args []interface{}, opts eval.RawOptions, inputs eval.Inputs) ([]interface{}, error) {
    93  	out := makeComplexCandidate(opts, args[0].(string))
    94  	return []interface{}{out}, nil
    95  }
    96  
    97  func makeArgCompleter() hashmap.Map {
    98  	m := vals.EmptyMap
    99  	for k, v := range argCompletersData {
   100  		m = m.Assoc(k, &builtinArgCompleter{v.name, v.impl, m})
   101  	}
   102  	return m
   103  }
   104  
   105  func (c *completion) Teardown() {
   106  	c.completionState = completionState{}
   107  }
   108  
   109  func (c *completion) Binding(k ui.Key) eval.Callable {
   110  	return c.binding.GetOrDefault(k)
   111  }
   112  
   113  func (c *completion) Replacement() (int, int, string) {
   114  	return c.begin, c.end, c.selectedCandidate().code
   115  }
   116  
   117  func (*completion) RedrawModeLine() {}
   118  
   119  func (c *completion) needScrollbar() bool {
   120  	return c.firstShown > 0 || c.lastShownInFull < len(c.filtered)-1
   121  }
   122  
   123  func (c *completion) ModeLine() ui.Renderer {
   124  	ml := ui.NewModeLineRenderer(
   125  		fmt.Sprintf(" COMPLETING %s ", c.completer), c.filter)
   126  	if !c.needScrollbar() {
   127  		return ml
   128  	}
   129  	return ui.NewModeLineWithScrollBarRenderer(ml,
   130  		len(c.filtered), c.firstShown, c.lastShownInFull+1)
   131  }
   132  
   133  func (c *completion) CursorOnModeLine() bool {
   134  	return c.filtering
   135  }
   136  
   137  func (c *completion) left() {
   138  	if x := c.selected - c.height; x >= 0 {
   139  		c.selected = x
   140  	}
   141  }
   142  
   143  func (c *completion) right() {
   144  	if x := c.selected + c.height; x < len(c.filtered) {
   145  		c.selected = x
   146  	}
   147  }
   148  
   149  // acceptCompletion accepts currently selected completion candidate.
   150  func (c *completion) accept(ed eddefs.Editor) {
   151  	if 0 <= c.selected && c.selected < len(c.filtered) {
   152  		ed.SetBuffer(c.apply(ed.Buffer()))
   153  	}
   154  	ed.SetModeInsert()
   155  }
   156  
   157  func (c *completion) complDefault(ed eddefs.Editor) {
   158  	k := ed.LastKey()
   159  	if c.filtering && likeChar(k) {
   160  		c.changeFilter(c.filter + string(k.Rune))
   161  	} else if c.filtering && k == (ui.Key{ui.Backspace, 0}) {
   162  		_, size := utf8.DecodeLastRuneInString(c.filter)
   163  		if size > 0 {
   164  			c.changeFilter(c.filter[:len(c.filter)-size])
   165  		}
   166  	} else {
   167  		c.accept(ed)
   168  		ed.SetAction(eddefs.ReprocessKey)
   169  	}
   170  }
   171  
   172  func (c *completion) triggerFilter() {
   173  	if c.filtering {
   174  		c.filtering = false
   175  		c.changeFilter("")
   176  	} else {
   177  		c.filtering = true
   178  	}
   179  }
   180  
   181  func (c *completion) selectedCandidate() *candidate {
   182  	if c.selected == -1 {
   183  		return &candidate{}
   184  	}
   185  	return c.filtered[c.selected]
   186  }
   187  
   188  // apply returns the line and dot after applying a candidate.
   189  func (c *completion) apply(line string, dot int) (string, int) {
   190  	text := c.selectedCandidate().code
   191  	return line[:c.begin] + text + line[c.end:], c.begin + len(text)
   192  }
   193  
   194  func (c *completion) prev(cycle bool) {
   195  	c.selected--
   196  	if c.selected == -1 {
   197  		if cycle {
   198  			c.selected = len(c.filtered) - 1
   199  		} else {
   200  			c.selected++
   201  		}
   202  	}
   203  }
   204  
   205  func (c *completion) next(cycle bool) {
   206  	c.selected++
   207  	if c.selected == len(c.filtered) {
   208  		if cycle {
   209  			c.selected = 0
   210  		} else {
   211  			c.selected--
   212  		}
   213  	}
   214  }
   215  
   216  func (c *completion) start(ed eddefs.Editor, acceptSingleton bool) {
   217  	_, dot := ed.Buffer()
   218  	chunk := ed.ParsedBuffer()
   219  	node := parseutil.FindLeafNode(chunk, dot)
   220  	if node == nil {
   221  		return
   222  	}
   223  
   224  	completer, complSpec, err := complete(
   225  		node, &complEnv{ed.Evaler(), c.matcher, c.argCompleter})
   226  
   227  	if err != nil {
   228  		ed.AddTip("%v", err)
   229  		// We don't show the full stack trace. To make debugging still possible,
   230  		// we log it.
   231  		if pprinter, ok := err.(util.Pprinter); ok {
   232  			logger.Println("matcher error:")
   233  			logger.Println(pprinter.Pprint(""))
   234  		}
   235  	} else if completer == "" {
   236  		ed.AddTip("unsupported completion :(")
   237  		logger.Println("path to current leaf, leaf first")
   238  		for n := node; n != nil; n = n.Parent() {
   239  			logger.Printf("%T (%d-%d)", n, n.Begin(), n.End())
   240  		}
   241  	} else if len(complSpec.candidates) == 0 {
   242  		ed.AddTip("no candidate for %s", completer)
   243  	} else {
   244  		if acceptSingleton && len(complSpec.candidates) == 1 {
   245  			// Just accept this single candidate.
   246  			repl := complSpec.candidates[0].code
   247  			buffer, _ := ed.Buffer()
   248  			ed.SetBuffer(
   249  				buffer[:complSpec.begin]+repl+buffer[complSpec.end:],
   250  				complSpec.begin+len(repl))
   251  			return
   252  		}
   253  		c.completionState = completionState{
   254  			completer: completer,
   255  			complSpec: *complSpec,
   256  			filtered:  complSpec.candidates,
   257  		}
   258  		ed.SetMode(c)
   259  	}
   260  }
   261  
   262  // commonPrefix returns the longest common prefix of two strings.
   263  func commonPrefix(s, t string) string {
   264  	for i, r := range s {
   265  		if i >= len(t) {
   266  			return s[:i]
   267  		}
   268  		r2, _ := utf8.DecodeRuneInString(t[i:])
   269  		if r2 != r {
   270  			return s[:i]
   271  		}
   272  	}
   273  	return s
   274  }
   275  
   276  const (
   277  	completionColMarginLeft  = 1
   278  	completionColMarginRight = 1
   279  	completionColMarginTotal = completionColMarginLeft + completionColMarginRight
   280  )
   281  
   282  // maxWidth finds the maximum wcwidth of display texts of candidates [lo, hi).
   283  // hi may be larger than the number of candidates, in which case it is truncated
   284  // to the number of candidates.
   285  func (c *completion) maxWidth(lo, hi int) int {
   286  	if hi > len(c.filtered) {
   287  		hi = len(c.filtered)
   288  	}
   289  	width := 0
   290  	for i := lo; i < hi; i++ {
   291  		w := util.Wcswidth(c.filtered[i].menu.Text)
   292  		if width < w {
   293  			width = w
   294  		}
   295  	}
   296  	return width
   297  }
   298  
   299  func (c *completion) ListRender(width, maxHeight int) *ui.Buffer {
   300  	b := ui.NewBuffer(width)
   301  	cands := c.filtered
   302  	if len(cands) == 0 {
   303  		b.WriteString(util.TrimWcwidth("(no result)", width), "")
   304  		return b
   305  	}
   306  	if maxHeight <= 1 || width <= 2 {
   307  		b.WriteString(util.TrimWcwidth("(terminal too small)", width), "")
   308  		return b
   309  	}
   310  
   311  	// Reserve the the rightmost row as margins.
   312  	width--
   313  
   314  	// Determine comp.height and comp.firstShown.
   315  	// First determine whether all candidates can be fit in the screen,
   316  	// assuming that they are all of maximum width. If that is the case, we use
   317  	// the computed height as the height for the listing, and the first
   318  	// candidate to show is 0. Otherwise, we use min(height, len(cands)) as the
   319  	// height and find the first candidate to show.
   320  	perLine := max(1, width/(c.maxWidth(0, len(cands))+completionColMarginTotal))
   321  	heightBound := util.CeilDiv(len(cands), perLine)
   322  	first := 0
   323  	height := 0
   324  	if heightBound < maxHeight {
   325  		height = heightBound
   326  	} else {
   327  		height = min(maxHeight, len(cands))
   328  		// Determine the first column to show. We start with the column in which the
   329  		// selected one is found, moving to the left until either the width is
   330  		// exhausted, or the old value of firstShown has been hit.
   331  		first = c.selected / height * height
   332  		w := c.maxWidth(first, first+height) + completionColMarginTotal
   333  		for ; first > c.firstShown; first -= height {
   334  			dw := c.maxWidth(first-height, first) + completionColMarginTotal
   335  			if w+dw > width {
   336  				break
   337  			}
   338  			w += dw
   339  		}
   340  	}
   341  	c.height = height
   342  	c.firstShown = first
   343  
   344  	var i, j int
   345  	remainedWidth := width
   346  	trimmed := false
   347  	// Show the results in columns, until width is exceeded.
   348  	for i = first; i < len(cands); i += height {
   349  		// Determine the width of the column (without the margin)
   350  		colWidth := c.maxWidth(i, min(i+height, len(cands)))
   351  		totalColWidth := colWidth + completionColMarginTotal
   352  		if totalColWidth > remainedWidth {
   353  			totalColWidth = remainedWidth
   354  			colWidth = totalColWidth - completionColMarginTotal
   355  			trimmed = true
   356  		}
   357  
   358  		col := ui.NewBuffer(totalColWidth)
   359  		for j = i; j < i+height; j++ {
   360  			if j > i {
   361  				col.Newline()
   362  			}
   363  			if j >= len(cands) {
   364  				// Write padding to make the listing a rectangle.
   365  				col.WriteSpaces(totalColWidth, styleForCompletion.String())
   366  			} else {
   367  				col.WriteSpaces(completionColMarginLeft, styleForCompletion.String())
   368  				s := ui.JoinStyles(styleForCompletion, cands[j].menu.Styles)
   369  				if j == c.selected {
   370  					s = append(s, styleForSelectedCompletion.String())
   371  				}
   372  				col.WriteString(util.ForceWcwidth(cands[j].menu.Text, colWidth), s.String())
   373  				col.WriteSpaces(completionColMarginRight, styleForCompletion.String())
   374  				if !trimmed {
   375  					c.lastShownInFull = j
   376  				}
   377  			}
   378  		}
   379  
   380  		b.ExtendRight(col, 0)
   381  		remainedWidth -= totalColWidth
   382  		if remainedWidth <= completionColMarginTotal {
   383  			break
   384  		}
   385  	}
   386  	// When the listing is incomplete, always use up the entire width.
   387  	if remainedWidth > 0 && c.needScrollbar() {
   388  		col := ui.NewBuffer(remainedWidth)
   389  		for i := 0; i < height; i++ {
   390  			if i > 0 {
   391  				col.Newline()
   392  			}
   393  			col.WriteSpaces(remainedWidth, styleForCompletion.String())
   394  		}
   395  		b.ExtendRight(col, 0)
   396  	}
   397  	return b
   398  }
   399  
   400  func (c *completion) changeFilter(f string) {
   401  	c.filter = f
   402  	if f == "" {
   403  		c.filtered = c.candidates
   404  		return
   405  	}
   406  	c.filtered = nil
   407  	for _, cand := range c.candidates {
   408  		if strings.Contains(cand.menu.Text, f) {
   409  			c.filtered = append(c.filtered, cand)
   410  		}
   411  	}
   412  	if len(c.filtered) > 0 {
   413  		c.selected = 0
   414  	} else {
   415  		c.selected = -1
   416  	}
   417  }