src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/edit/completion.go (about) 1 package edit 2 3 import ( 4 "bufio" 5 "fmt" 6 "os" 7 "reflect" 8 "strings" 9 "sync" 10 "unicode/utf8" 11 12 "src.elv.sh/pkg/cli/modes" 13 "src.elv.sh/pkg/cli/tk" 14 "src.elv.sh/pkg/edit/complete" 15 "src.elv.sh/pkg/eval" 16 "src.elv.sh/pkg/eval/errs" 17 "src.elv.sh/pkg/eval/vals" 18 "src.elv.sh/pkg/eval/vars" 19 "src.elv.sh/pkg/parse" 20 "src.elv.sh/pkg/persistent/hash" 21 "src.elv.sh/pkg/strutil" 22 "src.elv.sh/pkg/ui" 23 ) 24 25 type complexCandidateOpts struct { 26 CodeSuffix string 27 Display any 28 } 29 30 func (*complexCandidateOpts) SetDefaultOptions() {} 31 32 func complexCandidate(fm *eval.Frame, opts complexCandidateOpts, stem string) (complexItem, error) { 33 var display ui.Text 34 switch displayOpt := opts.Display.(type) { 35 case nil: 36 // Leave display = nil 37 case string: 38 display = ui.T(displayOpt) 39 case ui.Text: 40 display = displayOpt 41 default: 42 return complexItem{}, errs.BadValue{What: "&display", 43 Valid: "string or styled", Actual: vals.ReprPlain(displayOpt)} 44 } 45 return complexItem{ 46 Stem: stem, 47 CodeSuffix: opts.CodeSuffix, 48 Display: display, 49 }, nil 50 } 51 52 func completionStart(ed *Editor, bindings tk.Bindings, ev *eval.Evaler, cfg complete.Config, smart bool) { 53 codeArea, ok := focusedCodeArea(ed.app) 54 if !ok { 55 return 56 } 57 if smart { 58 ed.applyAutofix() 59 } 60 buf := codeArea.CopyState().Buffer 61 result, err := complete.Complete( 62 complete.CodeBuffer{Content: buf.Content, Dot: buf.Dot}, ev, cfg) 63 if err != nil { 64 ed.app.Notify(modes.ErrorText(err)) 65 return 66 } 67 if smart { 68 prefix := "" 69 for i, item := range result.Items { 70 if i == 0 { 71 prefix = item.ToInsert 72 continue 73 } 74 prefix = commonPrefix(prefix, item.ToInsert) 75 if prefix == "" { 76 break 77 } 78 } 79 if prefix != "" { 80 insertedPrefix := false 81 codeArea.MutateState(func(s *tk.CodeAreaState) { 82 rep := s.Buffer.Content[result.Replace.From:result.Replace.To] 83 if len(prefix) > len(rep) && strings.HasPrefix(prefix, rep) { 84 s.Pending = tk.PendingCode{ 85 Content: prefix, 86 From: result.Replace.From, To: result.Replace.To} 87 s.ApplyPending() 88 insertedPrefix = true 89 } 90 }) 91 if insertedPrefix { 92 return 93 } 94 } 95 } 96 w, err := modes.NewCompletion(ed.app, modes.CompletionSpec{ 97 Name: result.Name, Replace: result.Replace, Items: result.Items, 98 Filter: filterSpec, Bindings: bindings, 99 }) 100 if w != nil { 101 ed.app.PushAddon(w) 102 } 103 if err != nil { 104 ed.app.Notify(modes.ErrorText(err)) 105 } 106 } 107 108 func initCompletion(ed *Editor, ev *eval.Evaler, nb eval.NsBuilder) { 109 bindingVar := newBindingVar(emptyBindingsMap) 110 bindings := newMapBindings(ed, ev, bindingVar) 111 matcherMapVar := newMapVar(vals.EmptyMap) 112 argGeneratorMapVar := newMapVar(vals.EmptyMap) 113 cfg := func() complete.Config { 114 return complete.Config{ 115 Filterer: adaptMatcherMap( 116 ed, ev, matcherMapVar.Get().(vals.Map)), 117 ArgGenerator: adaptArgGeneratorMap( 118 ev, argGeneratorMapVar.Get().(vals.Map)), 119 } 120 } 121 generateForSudo := func(args []string) ([]complete.RawItem, error) { 122 return complete.GenerateForSudo(args, ev, cfg()) 123 } 124 nb.AddGoFns(map[string]any{ 125 "complete-filename": wrapArgGenerator(complete.GenerateFileNames), 126 "complete-getopt": completeGetopt, 127 "complete-sudo": wrapArgGenerator(generateForSudo), 128 "complex-candidate": complexCandidate, 129 "match-prefix": wrapMatcher(strings.HasPrefix), 130 "match-subseq": wrapMatcher(strutil.HasSubseq), 131 "match-substr": wrapMatcher(strings.Contains), 132 }) 133 app := ed.app 134 nb.AddNs("completion", 135 eval.BuildNsNamed("edit:completion"). 136 AddVars(map[string]vars.Var{ 137 "arg-completer": argGeneratorMapVar, 138 "binding": bindingVar, 139 "matcher": matcherMapVar, 140 }). 141 AddGoFns(map[string]any{ 142 "accept": func() { listingAccept(app) }, 143 "smart-start": func() { completionStart(ed, bindings, ev, cfg(), true) }, 144 "start": func() { completionStart(ed, bindings, ev, cfg(), false) }, 145 "up": func() { listingUp(app) }, 146 "down": func() { listingDown(app) }, 147 "up-cycle": func() { listingUpCycle(app) }, 148 "down-cycle": func() { listingDownCycle(app) }, 149 "left": func() { listingLeft(app) }, 150 "right": func() { listingRight(app) }, 151 })) 152 } 153 154 // A wrapper type implementing Elvish value methods. 155 type complexItem complete.ComplexItem 156 157 func (c complexItem) Index(k any) (any, bool) { 158 switch k { 159 case "stem": 160 return c.Stem, true 161 case "code-suffix": 162 return c.CodeSuffix, true 163 case "display": 164 return c.Display, true 165 } 166 return nil, false 167 } 168 169 func (c complexItem) IterateKeys(f func(any) bool) { 170 vals.Feed(f, "stem", "code-suffix", "display") 171 } 172 173 func (c complexItem) Kind() string { return "map" } 174 175 func (c complexItem) Equal(a any) bool { 176 rhs, ok := a.(complexItem) 177 return ok && c.Stem == rhs.Stem && 178 c.CodeSuffix == rhs.CodeSuffix && reflect.DeepEqual(c.Display, rhs.Display) 179 } 180 181 func (c complexItem) Hash() uint32 { 182 h := hash.DJBInit 183 h = hash.DJBCombine(h, hash.String(c.Stem)) 184 h = hash.DJBCombine(h, hash.String(c.CodeSuffix)) 185 // TODO: Add c.Display 186 return h 187 } 188 189 func (c complexItem) Repr(indent int) string { 190 // TODO(xiaq): Pretty-print when indent >= 0 191 return fmt.Sprintf("(edit:complex-candidate %s &code-suffix=%s &display=%s)", 192 parse.Quote(c.Stem), parse.Quote(c.CodeSuffix), vals.Repr(c.Display, indent+1)) 193 } 194 195 type wrappedArgGenerator func(*eval.Frame, ...string) error 196 197 // Wraps an ArgGenerator into a function that can be then passed to 198 // eval.NewGoFn. 199 func wrapArgGenerator(gen complete.ArgGenerator) wrappedArgGenerator { 200 return func(fm *eval.Frame, args ...string) error { 201 rawItems, err := gen(args) 202 if err != nil { 203 return err 204 } 205 out := fm.ValueOutput() 206 for _, rawItem := range rawItems { 207 var v any 208 switch rawItem := rawItem.(type) { 209 case complete.ComplexItem: 210 v = complexItem(rawItem) 211 case complete.PlainItem: 212 v = string(rawItem) 213 default: 214 v = rawItem 215 } 216 err := out.Put(v) 217 if err != nil { 218 return err 219 } 220 } 221 return nil 222 } 223 } 224 225 func commonPrefix(s1, s2 string) string { 226 for i, r := range s1 { 227 if s2 == "" { 228 break 229 } 230 r2, n2 := utf8.DecodeRuneInString(s2) 231 if r2 != r { 232 return s1[:i] 233 } 234 s2 = s2[n2:] 235 } 236 return s1 237 } 238 239 // The type for a native Go matcher. This is not equivalent to the Elvish 240 // counterpart, which streams input and output. This is because we can actually 241 // afford calling a Go function for each item, so omitting the streaming 242 // behavior makes the implementation simpler. 243 // 244 // Native Go matchers are wrapped into Elvish matchers, but never the other way 245 // around. 246 // 247 // This type is satisfied by strings.Contains and strings.HasPrefix; they are 248 // wrapped into match-substr and match-prefix respectively. 249 type matcher func(text, seed string) bool 250 251 type matcherOpts struct { 252 IgnoreCase bool 253 SmartCase bool 254 } 255 256 func (*matcherOpts) SetDefaultOptions() {} 257 258 type wrappedMatcher func(fm *eval.Frame, opts matcherOpts, seed string, inputs eval.Inputs) error 259 260 func wrapMatcher(m matcher) wrappedMatcher { 261 return func(fm *eval.Frame, opts matcherOpts, seed string, inputs eval.Inputs) error { 262 out := fm.ValueOutput() 263 var errOut error 264 if opts.IgnoreCase || (opts.SmartCase && seed == strings.ToLower(seed)) { 265 if opts.IgnoreCase { 266 seed = strings.ToLower(seed) 267 } 268 inputs(func(v any) { 269 if errOut != nil { 270 return 271 } 272 errOut = out.Put(m(strings.ToLower(vals.ToString(v)), seed)) 273 }) 274 } else { 275 inputs(func(v any) { 276 if errOut != nil { 277 return 278 } 279 errOut = out.Put(m(vals.ToString(v), seed)) 280 }) 281 } 282 return errOut 283 } 284 } 285 286 // Adapts $edit:completion:matcher into a Filterer. 287 func adaptMatcherMap(nt notifier, ev *eval.Evaler, m vals.Map) complete.Filterer { 288 return func(ctxName, seed string, rawItems []complete.RawItem) []complete.RawItem { 289 matcher, ok := lookupFn(m, ctxName) 290 if !ok { 291 nt.notifyf( 292 "matcher for %s not a function, falling back to prefix matching", ctxName) 293 } 294 if matcher == nil { 295 return complete.FilterPrefix(ctxName, seed, rawItems) 296 } 297 input := make(chan any) 298 stopInputFeeder := make(chan struct{}) 299 defer close(stopInputFeeder) 300 // Feed a string representing all raw candidates to the input channel. 301 go func() { 302 defer close(input) 303 for _, rawItem := range rawItems { 304 select { 305 case input <- rawItem.String(): 306 case <-stopInputFeeder: 307 return 308 } 309 } 310 }() 311 312 // TODO: Supply the Chan component of port 2. 313 port1, collect, err := eval.ValueCapturePort() 314 if err != nil { 315 nt.notifyf("cannot create pipe to run completion matcher: %v", err) 316 return nil 317 } 318 319 err = ev.Call(matcher, 320 eval.CallCfg{Args: []any{seed}, From: "[editor matcher]"}, 321 eval.EvalCfg{Ports: []*eval.Port{ 322 // TODO: Supply the Chan component of port 2. 323 {Chan: input, File: eval.DevNull}, port1, {File: os.Stderr}}}) 324 outputs := collect() 325 326 if err != nil { 327 nt.notifyError("matcher", err) 328 // Continue with whatever values have been output 329 } 330 if len(outputs) != len(rawItems) { 331 nt.notifyf( 332 "matcher has output %v values, not equal to %v inputs", 333 len(outputs), len(rawItems)) 334 } 335 filtered := []complete.RawItem{} 336 for i := 0; i < len(rawItems) && i < len(outputs); i++ { 337 if vals.Bool(outputs[i]) { 338 filtered = append(filtered, rawItems[i]) 339 } 340 } 341 return filtered 342 } 343 } 344 345 func adaptArgGeneratorMap(ev *eval.Evaler, m vals.Map) complete.ArgGenerator { 346 return func(args []string) ([]complete.RawItem, error) { 347 gen, ok := lookupFn(m, args[0]) 348 if !ok { 349 return nil, fmt.Errorf("arg completer for %s not a function", args[0]) 350 } 351 if gen == nil { 352 return complete.GenerateFileNames(args) 353 } 354 argValues := make([]any, len(args)) 355 for i, arg := range args { 356 argValues[i] = arg 357 } 358 var output []complete.RawItem 359 var outputMutex sync.Mutex 360 collect := func(item complete.RawItem) { 361 outputMutex.Lock() 362 defer outputMutex.Unlock() 363 output = append(output, item) 364 } 365 valueCb := func(ch <-chan any) { 366 for v := range ch { 367 switch v := v.(type) { 368 case string: 369 collect(complete.PlainItem(v)) 370 case complexItem: 371 collect(complete.ComplexItem(v)) 372 default: 373 collect(complete.PlainItem(vals.ToString(v))) 374 } 375 } 376 } 377 bytesCb := func(r *os.File) { 378 buffered := bufio.NewReader(r) 379 for { 380 line, err := buffered.ReadString('\n') 381 if line != "" { 382 collect(complete.PlainItem(strutil.ChopLineEnding(line))) 383 } 384 if err != nil { 385 break 386 } 387 } 388 } 389 port1, done, err := eval.PipePort(valueCb, bytesCb) 390 if err != nil { 391 panic(err) 392 } 393 err = ev.Call(gen, 394 eval.CallCfg{Args: argValues, From: "[editor arg generator]"}, 395 eval.EvalCfg{Ports: []*eval.Port{ 396 // TODO: Supply the Chan component of port 2. 397 nil, port1, {File: os.Stderr}}}) 398 done() 399 400 return output, err 401 } 402 } 403 404 func lookupFn(m vals.Map, ctxName string) (eval.Callable, bool) { 405 val, ok := m.Index(ctxName) 406 if !ok { 407 val, ok = m.Index("") 408 } 409 if !ok { 410 // No matcher, but not an error either 411 return nil, true 412 } 413 fn, ok := val.(eval.Callable) 414 if !ok { 415 return nil, false 416 } 417 return fn, true 418 }