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 }