forked from elves/elvish
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcompletion.go
More file actions
603 lines (567 loc) · 16.4 KB
/
Copy pathcompletion.go
File metadata and controls
603 lines (567 loc) · 16.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
package edit
import (
"bufio"
"fmt"
"os"
"reflect"
"strings"
"sync"
"unicode/utf8"
"src.elv.sh/pkg/cli/modes"
"src.elv.sh/pkg/cli/tk"
"src.elv.sh/pkg/edit/complete"
"src.elv.sh/pkg/eval"
"src.elv.sh/pkg/eval/errs"
"src.elv.sh/pkg/eval/vals"
"src.elv.sh/pkg/eval/vars"
"src.elv.sh/pkg/parse"
"src.elv.sh/pkg/persistent/hash"
"src.elv.sh/pkg/strutil"
"src.elv.sh/pkg/ui"
)
type complexCandidateOpts struct {
CodeSuffix string
Display any
}
func (*complexCandidateOpts) SetDefaultOptions() {}
func complexCandidate(fm *eval.Frame, opts complexCandidateOpts, stem string) (complexItem, error) {
var display ui.Text
switch displayOpt := opts.Display.(type) {
case nil:
// Leave display = nil
case string:
display = ui.T(displayOpt)
case ui.Text:
display = displayOpt
default:
return complexItem{}, errs.BadValue{What: "&display",
Valid: "string or styled", Actual: vals.ReprPlain(displayOpt)}
}
return complexItem{
Stem: stem,
CodeSuffix: opts.CodeSuffix,
Display: display,
}, nil
}
func completionStart(ed *Editor, bindings tk.Bindings, ev *eval.Evaler, cfg complete.Config, smart bool) {
codeArea, ok := focusedCodeArea(ed.app)
if !ok {
return
}
if smart {
ed.applyAutofix()
}
buf := codeArea.CopyState().Buffer
result, err := complete.Complete(
complete.CodeBuffer{Content: buf.Content, Dot: buf.Dot}, ev, cfg)
if err != nil {
ed.app.Notify(modes.ErrorText(err))
return
}
if smart {
// With a single candidate, insert it directly instead of opening
// the completion menu. This avoids a redundant menu when an
// external completion tool has already presented its own UI and
// returned one result.
if len(result.Items) == 1 {
codeArea.MutateState(func(s *tk.CodeAreaState) {
s.Pending = tk.PendingCode{
Content: result.Items[0].ToInsert,
From: result.Replace.From, To: result.Replace.To}
s.ApplyPending()
})
return
}
prefix := ""
for i, item := range result.Items {
if i == 0 {
prefix = item.ToInsert
continue
}
prefix = commonPrefix(prefix, item.ToInsert)
if prefix == "" {
break
}
}
if prefix != "" {
insertedPrefix := false
codeArea.MutateState(func(s *tk.CodeAreaState) {
rep := s.Buffer.Content[result.Replace.From:result.Replace.To]
if extendsSeed(prefix, rep, result.Seed, ev) {
s.Pending = tk.PendingCode{
Content: prefix,
From: result.Replace.From, To: result.Replace.To}
s.ApplyPending()
insertedPrefix = true
}
})
if insertedPrefix {
return
}
}
}
w, err := modes.NewCompletion(ed.app, modes.CompletionSpec{
Name: result.Name, Replace: result.Replace, Items: result.Items,
Filter: filterSpec, Bindings: bindings,
})
if w != nil {
ed.app.PushAddon(w)
}
if err != nil {
ed.app.Notify(modes.ErrorText(err))
}
}
func initCompletion(ed *Editor, ev *eval.Evaler, nb eval.NsBuilder) {
bindingVar := newBindingVar(emptyBindingsMap)
bindings := newMapBindings(ed, ev, bindingVar)
matcherMapVar := newMapVar(vals.EmptyMap)
argGeneratorMapVar := newMapVar(vals.EmptyMap)
commandGeneratorVar := newFnVar(nil)
variableGeneratorVar := newFnVar(nil)
cfg := func() complete.Config {
return complete.Config{
Filterer: adaptMatcherMap(
ed, ev, matcherMapVar.Get().(vals.Map)),
ArgGenerator: adaptArgGeneratorMap(
ev, argGeneratorMapVar.Get().(vals.Map)),
CommandGenerator: adaptCommandGenerator(
ev, commandGeneratorVar.Get()),
VariableGenerator: adaptVariableGenerator(
ev, variableGeneratorVar.Get()),
}
}
generateForSudo := func(args []string) ([]complete.RawItem, error) {
return complete.GenerateForSudo(args, ev, cfg())
}
nb.AddGoFns(map[string]any{
"complete-filename": wrapArgGenerator(complete.GenerateFileNames),
"complete-dirname": wrapArgGenerator(complete.GenerateDirNames),
"complete-getopt": completeGetopt,
"complete-sudo": wrapArgGenerator(generateForSudo),
"complex-candidate": complexCandidate,
"match-prefix": wrapMatcher(strings.HasPrefix),
"match-subseq": wrapMatcher(strutil.HasSubseq),
"match-substr": wrapMatcher(strings.Contains),
})
app := ed.app
nb.AddNs("completion",
eval.BuildNsNamed("edit:completion").
AddVars(map[string]vars.Var{
"arg-completer": argGeneratorMapVar,
"binding": bindingVar,
"command-completer": commandGeneratorVar,
"matcher": matcherMapVar,
"variable-completer": variableGeneratorVar,
}).
AddGoFns(map[string]any{
"accept": func() { listingAccept(app) },
"smart-start": func() { completionStart(ed, bindings, ev, cfg(), true) },
"start": func() { completionStart(ed, bindings, ev, cfg(), false) },
"up": func() { listingUp(app) },
"down": func() { listingDown(app) },
"up-cycle": func() { listingUpCycle(app) },
"down-cycle": func() { listingDownCycle(app) },
"left": func() { listingLeft(app) },
"right": func() { listingRight(app) },
}))
}
// A wrapper type implementing Elvish value methods.
type complexItem complete.ComplexItem
func (c complexItem) Index(k any) (any, bool) {
switch k {
case "stem":
return c.Stem, true
case "code-suffix":
return c.CodeSuffix, true
case "display":
return c.Display, true
}
return nil, false
}
func (c complexItem) IterateKeys(f func(any) bool) {
vals.Feed(f, "stem", "code-suffix", "display")
}
func (c complexItem) Kind() string { return "map" }
func (c complexItem) Equal(a any) bool {
rhs, ok := a.(complexItem)
return ok && c.Stem == rhs.Stem &&
c.CodeSuffix == rhs.CodeSuffix && reflect.DeepEqual(c.Display, rhs.Display)
}
func (c complexItem) Hash() uint32 {
h := hash.DJBInit
h = hash.DJBCombine(h, hash.String(c.Stem))
h = hash.DJBCombine(h, hash.String(c.CodeSuffix))
// TODO: Add c.Display
return h
}
func (c complexItem) Repr(indent int) string {
// TODO(xiaq): Pretty-print when indent >= 0
return fmt.Sprintf("(edit:complex-candidate %s &code-suffix=%s &display=%s)",
parse.Quote(c.Stem), parse.Quote(c.CodeSuffix), vals.Repr(c.Display, indent+1))
}
type wrappedArgGenerator func(*eval.Frame, ...string) error
// Wraps an ArgGenerator into a function that can be then passed to
// eval.NewGoFn.
func wrapArgGenerator(gen complete.ArgGenerator) wrappedArgGenerator {
return func(fm *eval.Frame, args ...string) error {
rawItems, err := gen(args)
if err != nil {
return err
}
out := fm.ValueOutput()
for _, rawItem := range rawItems {
var v any
switch rawItem := rawItem.(type) {
case complete.ComplexItem:
v = complexItem(rawItem)
case complete.PlainItem:
v = string(rawItem)
default:
v = rawItem
}
err := out.Put(v)
if err != nil {
return err
}
}
return nil
}
}
func commonPrefix(s1, s2 string) string {
for i, r := range s1 {
if s2 == "" {
break
}
r2, n2 := utf8.DecodeRuneInString(s2)
if r2 != r {
return s1[:i]
}
s2 = s2[n2:]
}
return s1
}
// extendsSeed reports whether the quoted candidate prefix extends the text the
// user has already typed. The comparison is done on logical (unquoted) values
// rather than source syntax, so that a candidate like "name-with-space" is
// recognized as extending the partial input name-" (where the quote appears in
// a different position).
//
// prefix is the common quoted ToInsert of the candidates. rep is the raw source
// text being replaced. seed is the unquoted value of that source text (from
// complete.Result.Seed). If seed is empty, the comparison falls back to
// unquoting rep, to handle tilde expansion that seed may not cover.
func extendsSeed(prefix, rep, seed string, ev *eval.Evaler) bool {
candidateValue := unquoteValue(prefix, ev)
seedValue := seed
if seedValue == "" {
seedValue = unquoteValue(rep, ev)
}
return len(candidateValue) > len(seedValue) &&
strings.HasPrefix(strings.ToLower(candidateValue), strings.ToLower(seedValue))
}
// unquoteValue parses s as Elvish code and returns the purely-evaluated string
// value of the first compound. This strips quotes and expands tildes, yielding
// the logical value rather than the surface syntax. If parsing or evaluation
// fails, it returns s unchanged as a safe fallback.
func unquoteValue(s string, ev *eval.Evaler) string {
tree, _ := parse.Parse(parse.Source{Code: s}, parse.Config{})
var compound *parse.Compound
findFirstCompound(tree.Root, &compound)
if compound == nil {
return s
}
if val, ok := ev.PurelyEvalCompound(compound); ok {
return val
}
return s
}
// findFirstCompound searches the parse tree depth-first for the first Compound
// node and stores it in *result.
func findFirstCompound(n parse.Node, result **parse.Compound) {
if *result != nil {
return
}
if c, ok := n.(*parse.Compound); ok {
*result = c
return
}
for _, ch := range parse.Children(n) {
findFirstCompound(ch, result)
if *result != nil {
return
}
}
}
// The type for a native Go matcher. This is not equivalent to the Elvish
// counterpart, which streams input and output. This is because we can actually
// afford calling a Go function for each item, so omitting the streaming
// behavior makes the implementation simpler.
//
// Native Go matchers are wrapped into Elvish matchers, but never the other way
// around.
//
// This type is satisfied by strings.Contains and strings.HasPrefix; they are
// wrapped into match-substr and match-prefix respectively.
type matcher func(text, seed string) bool
type matcherOpts struct {
IgnoreCase bool
SmartCase bool
}
func (*matcherOpts) SetDefaultOptions() {}
type wrappedMatcher func(fm *eval.Frame, opts matcherOpts, seed string, inputs eval.Inputs) error
func wrapMatcher(m matcher) wrappedMatcher {
return func(fm *eval.Frame, opts matcherOpts, seed string, inputs eval.Inputs) error {
out := fm.ValueOutput()
var errOut error
if opts.IgnoreCase || (opts.SmartCase && seed == strings.ToLower(seed)) {
if opts.IgnoreCase {
seed = strings.ToLower(seed)
}
inputs(func(v any) {
if errOut != nil {
return
}
errOut = out.Put(m(strings.ToLower(vals.ToString(v)), seed))
})
} else {
inputs(func(v any) {
if errOut != nil {
return
}
errOut = out.Put(m(vals.ToString(v), seed))
})
}
return errOut
}
}
// Adapts $edit:completion:matcher into a Filterer.
func adaptMatcherMap(nt notifier, ev *eval.Evaler, m vals.Map) complete.Filterer {
return func(ctxName, seed string, rawItems []complete.RawItem) []complete.RawItem {
matcher, ok := lookupFn(m, ctxName)
if !ok {
nt.notifyf(
"matcher for %s not a function, falling back to prefix matching", ctxName)
}
if matcher == nil {
return complete.FilterPrefix(ctxName, seed, rawItems)
}
input := make(chan any)
stopInputFeeder := make(chan struct{})
defer close(stopInputFeeder)
// Feed a string representing all raw candidates to the input channel.
go func() {
defer close(input)
for _, rawItem := range rawItems {
select {
case input <- rawItem.String():
case <-stopInputFeeder:
return
}
}
}()
// TODO: Supply the Chan component of port 2.
port1, collect, err := eval.ValueCapturePort()
if err != nil {
nt.notifyf("cannot create pipe to run completion matcher: %v", err)
return nil
}
err = ev.Call(matcher,
eval.CallCfg{Args: []any{seed}, From: "[editor matcher]"},
eval.EvalCfg{Ports: []*eval.Port{
// TODO: Supply the Chan component of port 2.
{Chan: input, File: eval.DevNull}, port1, {File: os.Stderr}}})
outputs := collect()
if err != nil {
nt.notifyError("matcher", err)
// Continue with whatever values have been output
}
if len(outputs) != len(rawItems) {
nt.notifyf(
"matcher has output %v values, not equal to %v inputs",
len(outputs), len(rawItems))
}
filtered := []complete.RawItem{}
for i := 0; i < len(rawItems) && i < len(outputs); i++ {
if vals.Bool(outputs[i]) {
filtered = append(filtered, rawItems[i])
}
}
return filtered
}
}
func adaptArgGeneratorMap(ev *eval.Evaler, m vals.Map) complete.ArgGenerator {
return func(args []string) ([]complete.RawItem, error) {
gen, ok := lookupFn(m, args[0])
if !ok {
return nil, fmt.Errorf("arg completer for %s not a function", args[0])
}
if gen == nil {
return complete.GenerateFileNames(args)
}
argValues := make([]any, len(args))
for i, arg := range args {
argValues[i] = arg
}
var output []complete.RawItem
var outputMutex sync.Mutex
collect := func(item complete.RawItem) {
outputMutex.Lock()
defer outputMutex.Unlock()
output = append(output, item)
}
valueCb := func(ch <-chan any) {
for v := range ch {
switch v := v.(type) {
case string:
collect(complete.PlainItem(v))
case complexItem:
collect(complete.ComplexItem(v))
default:
collect(complete.PlainItem(vals.ToString(v)))
}
}
}
bytesCb := func(r *os.File) {
buffered := bufio.NewReader(r)
for {
line, err := buffered.ReadString('\n')
if line != "" {
collect(complete.PlainItem(strutil.ChopLineEnding(line)))
}
if err != nil {
break
}
}
}
port1, done, err := eval.PipePort(valueCb, bytesCb)
if err != nil {
panic(err)
}
err = ev.Call(gen,
eval.CallCfg{Args: argValues, From: "[editor arg generator]"},
eval.EvalCfg{Ports: []*eval.Port{
// TODO: Supply the Chan component of port 2.
nil, port1, {File: os.Stderr}}})
done()
return output, err
}
}
// adaptCommandGenerator adapts $edit:completion:command-completer into a
// complete.CommandGenerator. If the variable is nil (not set), nil is returned
// and the built-in generateCommands is used.
func adaptCommandGenerator(ev *eval.Evaler, v any) complete.CommandGenerator {
gen, ok := v.(eval.Callable)
if !ok || gen == nil {
return nil
}
return func(seed string) ([]complete.RawItem, error) {
var output []complete.RawItem
var outputMutex sync.Mutex
collect := func(item complete.RawItem) {
outputMutex.Lock()
defer outputMutex.Unlock()
output = append(output, item)
}
valueCb := func(ch <-chan any) {
for v := range ch {
switch v := v.(type) {
case string:
collect(complete.PlainItem(v))
case complexItem:
collect(complete.ComplexItem(v))
default:
collect(complete.PlainItem(vals.ToString(v)))
}
}
}
bytesCb := func(r *os.File) {
buffered := bufio.NewReader(r)
for {
line, err := buffered.ReadString('\n')
if line != "" {
collect(complete.PlainItem(strutil.ChopLineEnding(line)))
}
if err != nil {
break
}
}
}
port1, done, err := eval.PipePort(valueCb, bytesCb)
if err != nil {
panic(err)
}
err = ev.Call(gen,
eval.CallCfg{Args: []any{seed}, From: "[editor command generator]"},
eval.EvalCfg{Ports: []*eval.Port{
nil, port1, {File: os.Stderr}}})
done()
return output, err
}
}
// adaptVariableGenerator adapts $edit:completion:variable-completer into a
// complete.VariableGenerator. If the variable is nil (not set), nil is returned
// and the built-in variable enumeration is used.
func adaptVariableGenerator(ev *eval.Evaler, v any) complete.VariableGenerator {
gen, ok := v.(eval.Callable)
if !ok || gen == nil {
return nil
}
return func(seed, ns string) ([]complete.RawItem, error) {
var output []complete.RawItem
var outputMutex sync.Mutex
collect := func(item complete.RawItem) {
outputMutex.Lock()
defer outputMutex.Unlock()
output = append(output, item)
}
valueCb := func(ch <-chan any) {
for v := range ch {
switch v := v.(type) {
case string:
collect(complete.NoQuoteItem(parse.QuoteVariableName(v)))
case complexItem:
collect(complete.ComplexItem(v))
default:
collect(complete.NoQuoteItem(parse.QuoteVariableName(vals.ToString(v))))
}
}
}
bytesCb := func(r *os.File) {
buffered := bufio.NewReader(r)
for {
line, err := buffered.ReadString('\n')
if line != "" {
collect(complete.NoQuoteItem(parse.QuoteVariableName(strutil.ChopLineEnding(line))))
}
if err != nil {
break
}
}
}
port1, done, err := eval.PipePort(valueCb, bytesCb)
if err != nil {
panic(err)
}
err = ev.Call(gen,
eval.CallCfg{Args: []any{seed, ns}, From: "[editor variable generator]"},
eval.EvalCfg{Ports: []*eval.Port{
nil, port1, {File: os.Stderr}}})
done()
return output, err
}
}
func lookupFn(m vals.Map, ctxName string) (eval.Callable, bool) {
val, ok := m.Index(ctxName)
if !ok {
val, ok = m.Index("")
}
if !ok {
// No matcher, but not an error either
return nil, true
}
fn, ok := val.(eval.Callable)
if !ok {
return nil, false
}
return fn, true
}