github.com/elves/elvish@v0.15.0/pkg/edit/completion.go (about) 1 package edit 2 3 import ( 4 "bufio" 5 "fmt" 6 "os" 7 "strings" 8 "sync" 9 "unicode/utf8" 10 11 "github.com/elves/elvish/pkg/cli" 12 "github.com/elves/elvish/pkg/cli/addons/completion" 13 "github.com/elves/elvish/pkg/edit/complete" 14 "github.com/elves/elvish/pkg/eval" 15 "github.com/elves/elvish/pkg/eval/vals" 16 "github.com/elves/elvish/pkg/fsutil" 17 "github.com/elves/elvish/pkg/parse" 18 "github.com/elves/elvish/pkg/strutil" 19 "github.com/xiaq/persistent/hash" 20 ) 21 22 //elvdoc:var completion:arg-completer 23 // 24 // A map containing argument completers. 25 26 //elvdoc:var completion:binding 27 // 28 // Keybinding for the completion mode. 29 30 //elvdoc:var completion:matcher 31 // 32 // A map mapping from context names to matcher functions. See the 33 // [Matcher](#matcher) section. 34 35 //elvdoc:fn complete-filename 36 // 37 // ```elvish 38 // edit:complete-filename $args... 39 // ``` 40 // 41 // Produces a list of filenames found in the directory of the last argument. All 42 // other arguments are ignored. If the last argument does not contain a path 43 // (either absolute or relative to the current directory), then the current 44 // directory is used. Relevant files are output as `edit:complex-candidate` 45 // objects. 46 // 47 // This function is the default handler for any commands without 48 // explicit handlers in `$edit:completion:arg-completer`. See [Argument 49 // Completer](#argument-completer). 50 // 51 // Example: 52 // 53 // ```elvish-transcript 54 // ~> edit:complete-filename '' 55 // ▶ (edit:complex-candidate Applications &code-suffix=/ &style='01;34') 56 // ▶ (edit:complex-candidate Books &code-suffix=/ &style='01;34') 57 // ▶ (edit:complex-candidate Desktop &code-suffix=/ &style='01;34') 58 // ▶ (edit:complex-candidate Docsafe &code-suffix=/ &style='01;34') 59 // ▶ (edit:complex-candidate Documents &code-suffix=/ &style='01;34') 60 // ... 61 // ~> edit:complete-filename .elvish/ 62 // ▶ (edit:complex-candidate .elvish/aliases &code-suffix=/ &style='01;34') 63 // ▶ (edit:complex-candidate .elvish/db &code-suffix=' ' &style='') 64 // ▶ (edit:complex-candidate .elvish/epm-installed &code-suffix=' ' &style='') 65 // ▶ (edit:complex-candidate .elvish/lib &code-suffix=/ &style='01;34') 66 // ▶ (edit:complex-candidate .elvish/rc.elv &code-suffix=' ' &style='') 67 // ``` 68 69 //elvdoc:fn complex-candidate 70 // 71 // ```elvish 72 // edit:complex-candidate $stem &display='' &code-suffix='' 73 // ``` 74 // 75 // Builds a complex candidate. This is mainly useful in [argument 76 // completers](#argument-completer). 77 // 78 // The `&display` option controls how the candidate is shown in the UI. It can 79 // be a string or a [styled](builtin.html#styled) text. If it is empty, `$stem` 80 // is used. 81 // 82 // The `&code-suffix` option affects how the candidate is inserted into the code 83 // when it is accepted. By default, a quoted version of `$stem` is inserted. If 84 // `$code-suffix` is non-empty, it is added to that text, and the suffix is not 85 // quoted. 86 87 type complexCandidateOpts struct { 88 CodeSuffix string 89 Display string 90 } 91 92 func (*complexCandidateOpts) SetDefaultOptions() {} 93 94 func complexCandidate(fm *eval.Frame, opts complexCandidateOpts, stem string) complexItem { 95 display := opts.Display 96 if display == "" { 97 display = stem 98 } 99 return complexItem{ 100 Stem: stem, 101 CodeSuffix: opts.CodeSuffix, 102 Display: display, 103 } 104 } 105 106 //elvdoc:fn match-prefix 107 // 108 // ```elvish 109 // edit:match-prefix $seed $inputs? 110 // ``` 111 // 112 // For each input, outputs whether the input has $seed as a prefix. Uses the 113 // result of `to-string` for non-string inputs. 114 // 115 // Roughly equivalent to the following Elvish function, but more efficient: 116 // 117 // ```elvish 118 // use str 119 // fn match-prefix [seed @input]{ 120 // each [x]{ str:has-prefix (to-string $x) $seed } $@input 121 // } 122 // ``` 123 124 //elvdoc:fn match-subseq 125 // 126 // ```elvish 127 // edit:match-subseq $seed $inputs? 128 // ``` 129 // 130 // For each input, outputs whether the input has $seed as a 131 // [subsequence](https://en.wikipedia.org/wiki/Subsequence). Uses the result of 132 // `to-string` for non-string inputs. 133 134 //elvdoc:fn match-substr 135 // 136 // ```elvish 137 // edit:match-substr $seed $inputs? 138 // ``` 139 // 140 // For each input, outputs whether the input has $seed as a substring. Uses the 141 // result of `to-string` for non-string inputs. 142 // 143 // Roughly equivalent to the following Elvish function, but more efficient: 144 // 145 // ```elvish 146 // use str 147 // fn match-substr [seed @input]{ 148 // each [x]{ str:has-contains (to-string $x) $seed } $@input 149 // } 150 // ``` 151 152 //elvdoc:fn completion:start 153 // 154 // Start the completion mode. 155 156 //elvdoc:fn completion:smart-start 157 // 158 // Starts the completion mode. However, if all the candidates share a non-empty 159 // prefix and that prefix starts with the seed, inserts the prefix instead. 160 161 func completionStart(app cli.App, binding cli.Handler, cfg complete.Config, smart bool) { 162 buf := app.CodeArea().CopyState().Buffer 163 result, err := complete.Complete( 164 complete.CodeBuffer{Content: buf.Content, Dot: buf.Dot}, cfg) 165 if err != nil { 166 app.Notify(err.Error()) 167 return 168 } 169 if smart { 170 prefix := "" 171 for i, item := range result.Items { 172 if i == 0 { 173 prefix = item.ToInsert 174 continue 175 } 176 prefix = commonPrefix(prefix, item.ToInsert) 177 if prefix == "" { 178 break 179 } 180 } 181 if prefix != "" { 182 insertedPrefix := false 183 app.CodeArea().MutateState(func(s *cli.CodeAreaState) { 184 rep := s.Buffer.Content[result.Replace.From:result.Replace.To] 185 if len(prefix) > len(rep) && strings.HasPrefix(prefix, rep) { 186 s.Pending = cli.PendingCode{ 187 Content: prefix, 188 From: result.Replace.From, To: result.Replace.To} 189 s.ApplyPending() 190 insertedPrefix = true 191 } 192 }) 193 if insertedPrefix { 194 return 195 } 196 } 197 } 198 completion.Start(app, completion.Config{ 199 Name: result.Name, Replace: result.Replace, Items: result.Items, 200 Binding: binding}) 201 } 202 203 //elvdoc:fn completion:close 204 // 205 // Closes the completion mode UI. 206 207 func initCompletion(ed *Editor, ev *eval.Evaler, nb eval.NsBuilder) { 208 bindingVar := newBindingVar(EmptyBindingMap) 209 binding := newMapBinding(ed, ev, bindingVar) 210 matcherMapVar := newMapVar(vals.EmptyMap) 211 argGeneratorMapVar := newMapVar(vals.EmptyMap) 212 cfg := func() complete.Config { 213 return complete.Config{ 214 PureEvaler: pureEvaler{ev}, 215 Filterer: adaptMatcherMap( 216 ed, ev, matcherMapVar.Get().(vals.Map)), 217 ArgGenerator: adaptArgGeneratorMap( 218 ev, argGeneratorMapVar.Get().(vals.Map)), 219 } 220 } 221 generateForSudo := func(args []string) ([]complete.RawItem, error) { 222 return complete.GenerateForSudo(cfg(), args) 223 } 224 nb.AddGoFns("<edit>", map[string]interface{}{ 225 "complete-filename": wrapArgGenerator(complete.GenerateFileNames), 226 "complete-getopt": completeGetopt, 227 "complete-sudo": wrapArgGenerator(generateForSudo), 228 "complex-candidate": complexCandidate, 229 "match-prefix": wrapMatcher(strings.HasPrefix), 230 "match-subseq": wrapMatcher(strutil.HasSubseq), 231 "match-substr": wrapMatcher(strings.Contains), 232 }) 233 app := ed.app 234 nb.AddNs("completion", 235 eval.NsBuilder{ 236 "arg-completer": argGeneratorMapVar, 237 "binding": bindingVar, 238 "matcher": matcherMapVar, 239 }.AddGoFns("<edit:completion>:", map[string]interface{}{ 240 "accept": func() { listingAccept(app) }, 241 "smart-start": func() { completionStart(app, binding, cfg(), true) }, 242 "start": func() { completionStart(app, binding, cfg(), false) }, 243 "close": func() { completion.Close(app) }, 244 "up": func() { listingUp(app) }, 245 "down": func() { listingDown(app) }, 246 "up-cycle": func() { listingUpCycle(app) }, 247 "down-cycle": func() { listingDownCycle(app) }, 248 "left": func() { listingLeft(app) }, 249 "right": func() { listingRight(app) }, 250 }).Ns()) 251 } 252 253 // A wrapper type implementing Elvish value methods. 254 type complexItem complete.ComplexItem 255 256 func (c complexItem) Index(k interface{}) (interface{}, bool) { 257 switch k { 258 case "stem": 259 return c.Stem, true 260 case "code-suffix": 261 return c.CodeSuffix, true 262 case "display": 263 return c.Display, true 264 } 265 return nil, false 266 } 267 268 func (c complexItem) IterateKeys(f func(interface{}) bool) { 269 vals.Feed(f, "stem", "code-suffix", "display") 270 } 271 272 func (c complexItem) Kind() string { return "map" } 273 274 func (c complexItem) Equal(a interface{}) bool { 275 rhs, ok := a.(complexItem) 276 return ok && c.Stem == rhs.Stem && 277 c.CodeSuffix == rhs.CodeSuffix && c.Display == rhs.Display 278 } 279 280 func (c complexItem) Hash() uint32 { 281 h := hash.DJBInit 282 h = hash.DJBCombine(h, hash.String(c.Stem)) 283 h = hash.DJBCombine(h, hash.String(c.CodeSuffix)) 284 h = hash.DJBCombine(h, hash.String(c.Display)) 285 return h 286 } 287 288 func (c complexItem) Repr(indent int) string { 289 // TODO(xiaq): Pretty-print when indent >= 0 290 return fmt.Sprintf("(edit:complex-candidate %s &code-suffix=%s &display=%s)", 291 parse.Quote(c.Stem), parse.Quote(c.CodeSuffix), parse.Quote(c.Display)) 292 } 293 294 type wrappedArgGenerator func(*eval.Frame, ...string) error 295 296 // Wraps an ArgGenerator into a function that can be then passed to 297 // eval.NewGoFn. 298 func wrapArgGenerator(gen complete.ArgGenerator) wrappedArgGenerator { 299 return func(fm *eval.Frame, args ...string) error { 300 rawItems, err := gen(args) 301 if err != nil { 302 return err 303 } 304 ch := fm.OutputChan() 305 for _, rawItem := range rawItems { 306 switch rawItem := rawItem.(type) { 307 case complete.ComplexItem: 308 ch <- complexItem(rawItem) 309 case complete.PlainItem: 310 ch <- string(rawItem) 311 default: 312 ch <- rawItem 313 } 314 } 315 return nil 316 } 317 } 318 319 func commonPrefix(s1, s2 string) string { 320 for i, r := range s1 { 321 if s2 == "" { 322 break 323 } 324 r2, n2 := utf8.DecodeRuneInString(s2) 325 if r2 != r { 326 return s1[:i] 327 } 328 s2 = s2[n2:] 329 } 330 return s1 331 } 332 333 // The type for a native Go matcher. This is not equivalent to the Elvish 334 // counterpart, which streams input and output. This is because we can actually 335 // afford calling a Go function for each item, so omitting the streaming 336 // behavior makes the implementation simpler. 337 // 338 // Native Go matchers are wrapped into Elvish matchers, but never the other way 339 // around. 340 // 341 // This type is satisfied by strings.Contains and strings.HasPrefix; they are 342 // wrapped into match-substr and match-prefix respectively. 343 type matcher func(text, seed string) bool 344 345 type matcherOpts struct { 346 IgnoreCase bool 347 SmartCase bool 348 } 349 350 func (*matcherOpts) SetDefaultOptions() {} 351 352 type wrappedMatcher func(fm *eval.Frame, opts matcherOpts, seed string, inputs eval.Inputs) 353 354 func wrapMatcher(m matcher) wrappedMatcher { 355 return func(fm *eval.Frame, opts matcherOpts, seed string, inputs eval.Inputs) { 356 out := fm.OutputChan() 357 if opts.IgnoreCase || (opts.SmartCase && seed == strings.ToLower(seed)) { 358 if opts.IgnoreCase { 359 seed = strings.ToLower(seed) 360 } 361 inputs(func(v interface{}) { 362 out <- m(strings.ToLower(vals.ToString(v)), seed) 363 }) 364 } else { 365 inputs(func(v interface{}) { 366 out <- m(vals.ToString(v), seed) 367 }) 368 } 369 } 370 } 371 372 // Adapts $edit:completion:matcher into a Filterer. 373 func adaptMatcherMap(nt notifier, ev *eval.Evaler, m vals.Map) complete.Filterer { 374 return func(ctxName, seed string, rawItems []complete.RawItem) []complete.RawItem { 375 matcher, ok := lookupFn(m, ctxName) 376 if !ok { 377 nt.notifyf( 378 "matcher for %s not a function, falling back to prefix matching", ctxName) 379 } 380 if matcher == nil { 381 return complete.FilterPrefix(ctxName, seed, rawItems) 382 } 383 input := make(chan interface{}) 384 stopInputFeeder := make(chan struct{}) 385 defer close(stopInputFeeder) 386 // Feed a string representing all raw candidates to the input channel. 387 go func() { 388 defer close(input) 389 for _, rawItem := range rawItems { 390 select { 391 case input <- rawItem.String(): 392 case <-stopInputFeeder: 393 return 394 } 395 } 396 }() 397 398 // TODO: Supply the Chan component of port 2. 399 port1, collect, err := eval.CapturePort() 400 if err != nil { 401 nt.notifyf("cannot create pipe to run completion matcher: %v", err) 402 return nil 403 } 404 405 err = ev.Call(matcher, 406 eval.CallCfg{Args: []interface{}{seed}, From: "[editor matcher]"}, 407 eval.EvalCfg{Ports: []*eval.Port{ 408 // TODO: Supply the Chan component of port 2. 409 {Chan: input, File: eval.DevNull}, port1, {File: os.Stderr}}}) 410 outputs := collect() 411 412 if err != nil { 413 nt.notifyError("matcher", err) 414 // Continue with whatever values have been output 415 } 416 if len(outputs) != len(rawItems) { 417 nt.notifyf( 418 "matcher has output %v values, not equal to %v inputs", 419 len(outputs), len(rawItems)) 420 } 421 filtered := []complete.RawItem{} 422 for i := 0; i < len(rawItems) && i < len(outputs); i++ { 423 if vals.Bool(outputs[i]) { 424 filtered = append(filtered, rawItems[i]) 425 } 426 } 427 return filtered 428 } 429 } 430 431 func adaptArgGeneratorMap(ev *eval.Evaler, m vals.Map) complete.ArgGenerator { 432 return func(args []string) ([]complete.RawItem, error) { 433 gen, ok := lookupFn(m, args[0]) 434 if !ok { 435 return nil, fmt.Errorf("arg completer for %s not a function", args[0]) 436 } 437 if gen == nil { 438 return complete.GenerateFileNames(args) 439 } 440 argValues := make([]interface{}, len(args)) 441 for i, arg := range args { 442 argValues[i] = arg 443 } 444 var output []complete.RawItem 445 var outputMutex sync.Mutex 446 collect := func(item complete.RawItem) { 447 outputMutex.Lock() 448 defer outputMutex.Unlock() 449 output = append(output, item) 450 } 451 valueCb := func(ch <-chan interface{}) { 452 for v := range ch { 453 switch v := v.(type) { 454 case string: 455 collect(complete.PlainItem(v)) 456 case complexItem: 457 collect(complete.ComplexItem(v)) 458 default: 459 collect(complete.PlainItem(vals.ToString(v))) 460 } 461 } 462 } 463 bytesCb := func(r *os.File) { 464 buffered := bufio.NewReader(r) 465 for { 466 line, err := buffered.ReadString('\n') 467 if line != "" { 468 collect(complete.PlainItem(strutil.ChopLineEnding(line))) 469 } 470 if err != nil { 471 break 472 } 473 } 474 } 475 port1, done, err := eval.PipePort(valueCb, bytesCb) 476 if err != nil { 477 panic(err) 478 } 479 err = ev.Call(gen, 480 eval.CallCfg{Args: argValues, From: "[editor arg generator]"}, 481 eval.EvalCfg{Ports: []*eval.Port{ 482 // TODO: Supply the Chan component of port 2. 483 nil, port1, {File: os.Stderr}}}) 484 done() 485 486 return output, err 487 } 488 } 489 490 func lookupFn(m vals.Map, ctxName string) (eval.Callable, bool) { 491 val, ok := m.Index(ctxName) 492 if !ok { 493 val, ok = m.Index("") 494 } 495 if !ok { 496 // No matcher, but not an error either 497 return nil, true 498 } 499 fn, ok := val.(eval.Callable) 500 if !ok { 501 return nil, false 502 } 503 return fn, true 504 } 505 506 type pureEvaler struct{ ev *eval.Evaler } 507 508 func (pureEvaler) EachExternal(f func(string)) { fsutil.EachExternal(f) } 509 510 func (pureEvaler) EachSpecial(f func(string)) { 511 for name := range eval.IsBuiltinSpecial { 512 f(name) 513 } 514 } 515 516 func (pe pureEvaler) EachNs(f func(string)) { 517 eachNsInTop(pe.ev.Builtin(), pe.ev.Global(), f) 518 } 519 520 func (pe pureEvaler) EachVariableInNs(ns string, f func(string)) { 521 eachVariableInTop(pe.ev.Builtin(), pe.ev.Global(), ns, f) 522 } 523 524 func (pe pureEvaler) PurelyEvalPrimary(pn *parse.Primary) interface{} { 525 return pe.ev.PurelyEvalPrimary(pn) 526 } 527 528 func (pe pureEvaler) PurelyEvalCompound(cn *parse.Compound) (string, bool) { 529 return pe.ev.PurelyEvalCompound(cn) 530 } 531 532 func (pe pureEvaler) PurelyEvalPartialCompound(cn *parse.Compound, upto int) (string, bool) { 533 return pe.ev.PurelyEvalPartialCompound(cn, upto) 534 }