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