github.com/hoop33/elvish@v0.0.0-20160801152013-6d25485beab4/edit/completion.go (about)

     1  package edit
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  	"unicode/utf8"
     7  )
     8  
     9  // Completion subsystem.
    10  
    11  // Interface.
    12  
    13  type completion struct {
    14  	completer  string
    15  	begin, end int
    16  	all        []*candidate
    17  
    18  	filtering  bool
    19  	filter     string
    20  	candidates []*candidate
    21  	selected   int
    22  	firstShown int
    23  	lastShown  int
    24  	height     int
    25  }
    26  
    27  func (*completion) Mode() ModeType {
    28  	return modeCompletion
    29  }
    30  
    31  func (c *completion) ModeLine(width int) *buffer {
    32  	b := newBuffer(width)
    33  	b.writes(" ", "")
    34  	// Write title
    35  	title := fmt.Sprintf("COMPLETING %s", c.completer)
    36  	b.writes(TrimWcWidth(title, width), styleForMode)
    37  	// Write filter
    38  	if c.filtering {
    39  		b.writes(" ", "")
    40  		b.writes(c.filter, styleForFilter)
    41  		b.dot = b.cursor()
    42  	}
    43  	// Write horizontal scrollbar, using the remaining space
    44  	if c.firstShown > 0 || c.lastShown < len(c.candidates)-1 {
    45  		scrollbarWidth := width - lineWidth(b.cells[len(b.cells)-1]) - 2
    46  		if scrollbarWidth >= 3 {
    47  			b.writes(" ", "")
    48  			writeHorizontalScrollbar(b, len(c.candidates), c.firstShown, c.lastShown, scrollbarWidth)
    49  		}
    50  	}
    51  
    52  	return b
    53  }
    54  
    55  func startCompl(ed *Editor) {
    56  	startCompletionInner(ed, false)
    57  }
    58  
    59  func complPrefixOrStartCompl(ed *Editor) {
    60  	startCompletionInner(ed, true)
    61  }
    62  
    63  func complUp(ed *Editor) {
    64  	ed.completion.prev(false)
    65  }
    66  
    67  func complDown(ed *Editor) {
    68  	ed.completion.next(false)
    69  }
    70  
    71  func complLeft(ed *Editor) {
    72  	if c := ed.completion.selected - ed.completion.height; c >= 0 {
    73  		ed.completion.selected = c
    74  	}
    75  }
    76  
    77  func complRight(ed *Editor) {
    78  	if c := ed.completion.selected + ed.completion.height; c < len(ed.completion.candidates) {
    79  		ed.completion.selected = c
    80  	}
    81  }
    82  
    83  func complDownCycle(ed *Editor) {
    84  	ed.completion.next(true)
    85  }
    86  
    87  // acceptCompletion accepts currently selected completion candidate.
    88  func complAccept(ed *Editor) {
    89  	c := ed.completion
    90  	if 0 <= c.selected && c.selected < len(c.candidates) {
    91  		ed.line, ed.dot = c.apply(ed.line, ed.dot)
    92  	}
    93  	ed.mode = &ed.insert
    94  }
    95  
    96  func complDefault(ed *Editor) {
    97  	k := ed.lastKey
    98  	c := &ed.completion
    99  	if c.filtering && likeChar(k) {
   100  		c.changeFilter(c.filter + string(k.Rune))
   101  	} else if c.filtering && k == (Key{Backspace, 0}) {
   102  		_, size := utf8.DecodeLastRuneInString(c.filter)
   103  		if size > 0 {
   104  			c.changeFilter(c.filter[:len(c.filter)-size])
   105  		}
   106  	} else {
   107  		complAccept(ed)
   108  		ed.nextAction = action{typ: reprocessKey}
   109  	}
   110  }
   111  
   112  func complTriggerFilter(ed *Editor) {
   113  	c := &ed.completion
   114  	if c.filtering {
   115  		c.filtering = false
   116  		c.changeFilter("")
   117  	} else {
   118  		c.filtering = true
   119  	}
   120  }
   121  
   122  type candidate struct {
   123  	text    string
   124  	display styled
   125  	suffix  string
   126  }
   127  
   128  func (comp *completion) selectedCandidate() *candidate {
   129  	if comp.selected == -1 {
   130  		return &candidate{}
   131  	}
   132  	return comp.candidates[comp.selected]
   133  }
   134  
   135  // apply returns the line and dot after applying a candidate.
   136  func (comp *completion) apply(line string, dot int) (string, int) {
   137  	text := comp.selectedCandidate().text
   138  	return line[:comp.begin] + text + line[comp.end:], comp.begin + len(text)
   139  }
   140  
   141  func (c *completion) prev(cycle bool) {
   142  	c.selected--
   143  	if c.selected == -1 {
   144  		if cycle {
   145  			c.selected = len(c.candidates) - 1
   146  		} else {
   147  			c.selected++
   148  		}
   149  	}
   150  }
   151  
   152  func (c *completion) next(cycle bool) {
   153  	c.selected++
   154  	if c.selected == len(c.candidates) {
   155  		if cycle {
   156  			c.selected = 0
   157  		} else {
   158  			c.selected--
   159  		}
   160  	}
   161  }
   162  
   163  func startCompletionInner(ed *Editor, acceptPrefix bool) {
   164  	token := tokenAtDot(ed)
   165  	node := token.Node
   166  	if node == nil {
   167  		return
   168  	}
   169  
   170  	c := &completion{begin: -1}
   171  	for _, compl := range completers {
   172  		begin, end, candidates := compl.completer(node, ed)
   173  		if begin >= 0 {
   174  			c.completer = compl.name
   175  			c.begin, c.end, c.all = begin, end, candidates
   176  			c.candidates = c.all
   177  			break
   178  		}
   179  	}
   180  
   181  	if c.begin < 0 {
   182  		ed.addTip("unsupported completion :(")
   183  	} else if len(c.candidates) == 0 {
   184  		ed.addTip("no candidate for %s", c.completer)
   185  	} else {
   186  		if acceptPrefix {
   187  			// If there is a non-empty longest common prefix, insert it and
   188  			// don't start completion mode.
   189  			//
   190  			// As a special case, when there is exactly one candidate, it is
   191  			// immeidately accepted.
   192  			prefix := c.candidates[0].text
   193  			for _, cand := range c.candidates[1:] {
   194  				prefix = commonPrefix(prefix, cand.text)
   195  				if prefix == "" {
   196  					break
   197  				}
   198  			}
   199  			if prefix != "" && prefix != ed.line[c.begin:c.end] {
   200  				ed.line = ed.line[:c.begin] + prefix + ed.line[c.end:]
   201  				ed.dot = c.begin + len(prefix)
   202  				return
   203  			}
   204  		}
   205  		// Fix .display.text
   206  		for _, cand := range c.candidates {
   207  			if cand.display.text == "" {
   208  				cand.display.text = cand.text
   209  			}
   210  		}
   211  		ed.completion = *c
   212  		ed.mode = &ed.completion
   213  	}
   214  }
   215  
   216  func tokenAtDot(ed *Editor) Token {
   217  	if len(ed.tokens) == 0 || ed.dot > len(ed.line) {
   218  		return Token{}
   219  	}
   220  	if ed.dot == len(ed.line) {
   221  		return ed.tokens[len(ed.tokens)-1]
   222  	}
   223  	for _, token := range ed.tokens {
   224  		if ed.dot < token.Node.End() {
   225  			return token
   226  		}
   227  	}
   228  	return Token{}
   229  }
   230  
   231  // commonPrefix returns the longest common prefix of two strings.
   232  func commonPrefix(s, t string) string {
   233  	for i, r := range s {
   234  		if i >= len(t) {
   235  			return s[:i]
   236  		}
   237  		r2, _ := utf8.DecodeRuneInString(t[i:])
   238  		if r2 != r {
   239  			return s[:i]
   240  		}
   241  	}
   242  	return s
   243  }
   244  
   245  const completionListingColMargin = 2
   246  
   247  // maxWidth finds the maximum wcwidth of display texts of candidates [lo, hi).
   248  // hi may be larger than the number of candidates, in which case it is truncated
   249  // to the number of candidates.
   250  func (comp *completion) maxWidth(lo, hi int) int {
   251  	if hi > len(comp.candidates) {
   252  		hi = len(comp.candidates)
   253  	}
   254  	width := 0
   255  	for i := lo; i < hi; i++ {
   256  		w := WcWidths(comp.candidates[i].display.text)
   257  		if width < w {
   258  			width = w
   259  		}
   260  	}
   261  	return width
   262  }
   263  
   264  func (comp *completion) List(width, height int) *buffer {
   265  	b := newBuffer(width)
   266  	cands := comp.candidates
   267  	if len(cands) == 0 {
   268  		b.writes(TrimWcWidth("(no result)", width), "")
   269  		return b
   270  	}
   271  	if height <= 1 || width <= 2 {
   272  		b.writes(TrimWcWidth("(terminal too small)", width), "")
   273  		return b
   274  	}
   275  
   276  	comp.height = min(height, len(cands))
   277  
   278  	// Determine the first column to show. We start with the column in which the
   279  	// selected one is found, moving to the left until either the width is
   280  	// exhausted, or the old value of firstShown has been hit.
   281  	first := comp.selected / height * height
   282  	w := comp.maxWidth(first, first+height)
   283  	for ; first > comp.firstShown; first -= height {
   284  		dw := comp.maxWidth(first-height, first) + completionListingColMargin
   285  		if w+dw > width-2 {
   286  			break
   287  		}
   288  		w += dw
   289  	}
   290  	comp.firstShown = first
   291  
   292  	var i, j int
   293  	remainedWidth := width - 2
   294  	margin := 0
   295  	// Show the results in columns, until width is exceeded.
   296  	for i = first; i < len(cands); i += height {
   297  		if i > first {
   298  			margin = completionListingColMargin
   299  		}
   300  		// Determine the width of the column (without the margin)
   301  		colWidth := comp.maxWidth(i, min(i+height, len(cands)))
   302  		if colWidth > remainedWidth-margin {
   303  			colWidth = remainedWidth - margin
   304  		}
   305  
   306  		col := newBuffer(margin + colWidth)
   307  		for j = i; j < i+height && j < len(cands); j++ {
   308  			if j > i {
   309  				col.newline()
   310  			}
   311  			col.writePadding(margin, "")
   312  			style := cands[j].display.style
   313  			if j == comp.selected {
   314  				style = joinStyle(style, styleForSelected)
   315  			}
   316  			col.writes(ForceWcWidth(cands[j].display.text, colWidth), style)
   317  		}
   318  
   319  		b.extendHorizontal(col, 1)
   320  		remainedWidth -= colWidth + margin
   321  		if remainedWidth <= completionListingColMargin {
   322  			break
   323  		}
   324  	}
   325  	comp.lastShown = j - 1
   326  	return b
   327  }
   328  
   329  func (c *completion) changeFilter(f string) {
   330  	c.filter = f
   331  	if f == "" {
   332  		c.candidates = c.all
   333  		return
   334  	}
   335  	c.candidates = nil
   336  	for _, cand := range c.all {
   337  		if strings.Contains(cand.display.text, f) {
   338  			c.candidates = append(c.candidates, cand)
   339  		}
   340  	}
   341  	if len(c.candidates) > 0 {
   342  		c.selected = 0
   343  	} else {
   344  		c.selected = -1
   345  	}
   346  }