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 }