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 }