github.com/hoop33/elvish@v0.0.0-20160801152013-6d25485beab4/edit/completers.go (about) 1 package edit 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "os" 7 "path" 8 "sort" 9 "strings" 10 "unicode/utf8" 11 12 "github.com/elves/elvish/eval" 13 "github.com/elves/elvish/parse" 14 "github.com/elves/elvish/util" 15 "github.com/elves/getopt" 16 ) 17 18 // completer takes the current Node (always a leaf in the AST) and an Editor, 19 // and should returns an interval and a list of candidates, meaning that the 20 // text within the interval may be replaced by any of the candidates. If the 21 // completer is not applicable, it should return an invalid interval [-1, end). 22 type completer func(parse.Node, *Editor) (int, int, []*candidate) 23 24 var completers = []struct { 25 name string 26 completer 27 }{ 28 {"variable", complVariable}, 29 {"command name", complFormHead}, 30 {"argument", complArg}, 31 } 32 33 func complVariable(n parse.Node, ed *Editor) (int, int, []*candidate) { 34 begin, end := n.Begin(), n.End() 35 36 primary, ok := n.(*parse.Primary) 37 if !ok || primary.Type != parse.Variable { 38 return -1, -1, nil 39 } 40 41 splice, ns, head := eval.ParseVariable(primary.Value) 42 43 // Collect matching variables. 44 var varnames []string 45 iterateVariables(ed.evaler, ns, func(varname string) { 46 if strings.HasPrefix(varname, head) { 47 varnames = append(varnames, varname) 48 } 49 }) 50 sort.Strings(varnames) 51 52 cands := make([]*candidate, len(varnames)) 53 // Build candidates. 54 for i, varname := range varnames { 55 cands[i] = &candidate{text: "$" + eval.MakeVariableName(splice, ns, varname)} 56 } 57 return begin, end, cands 58 } 59 60 func iterateVariables(ev *eval.Evaler, ns string, f func(string)) { 61 if ns == "" { 62 for varname := range eval.Builtin() { 63 f(varname) 64 } 65 for varname := range ev.Global { 66 f(varname) 67 } 68 // TODO Include local names as well. 69 } else { 70 for varname := range ev.Modules[ns] { 71 f(varname) 72 } 73 } 74 } 75 76 func complFormHead(n parse.Node, ed *Editor) (int, int, []*candidate) { 77 begin, end, head, q := findFormHeadContext(n) 78 if begin == -1 { 79 return -1, -1, nil 80 } 81 cands, err := complFormHeadInner(head, ed) 82 if err != nil { 83 ed.notify("%v", err) 84 } 85 fixCandidates(cands, q) 86 return begin, end, cands 87 } 88 89 func findFormHeadContext(n parse.Node) (int, int, string, parse.PrimaryType) { 90 _, isChunk := n.(*parse.Chunk) 91 _, isPipeline := n.(*parse.Pipeline) 92 if isChunk || isPipeline { 93 return n.Begin(), n.End(), "", parse.Bareword 94 } 95 96 if primary, ok := n.(*parse.Primary); ok { 97 if compound, head := primaryInSimpleCompound(primary); compound != nil { 98 if form, ok := compound.Parent().(*parse.Form); ok { 99 if form.Head == compound { 100 return compound.Begin(), compound.End(), head, primary.Type 101 } 102 } 103 } 104 } 105 return -1, -1, "", 0 106 } 107 108 func complFormHeadInner(head string, ed *Editor) ([]*candidate, error) { 109 if util.DontSearch(head) { 110 return complFilenameInner(head, true) 111 } 112 113 var commands []string 114 got := func(s string) { 115 if strings.HasPrefix(s, head) { 116 commands = append(commands, s) 117 } 118 } 119 for special := range isBuiltinSpecial { 120 got(special) 121 } 122 splice, ns, _ := eval.ParseVariable(head) 123 iterateVariables(ed.evaler, ns, func(varname string) { 124 if strings.HasPrefix(varname, eval.FnPrefix) { 125 got(eval.MakeVariableName(splice, ns, varname[len(eval.FnPrefix):])) 126 } 127 }) 128 for command := range ed.isExternal { 129 got(command) 130 } 131 sort.Strings(commands) 132 133 cands := []*candidate{} 134 for _, cmd := range commands { 135 cands = append(cands, &candidate{text: cmd}) 136 } 137 return cands, nil 138 } 139 140 func complArg(n parse.Node, ed *Editor) (int, int, []*candidate) { 141 begin, end, current, q, form := findArgContext(n) 142 if begin == -1 { 143 return -1, -1, nil 144 } 145 146 // Find out head of the form and preceding arguments. 147 // If Form.Head is not a simple compound, head will be "", just what we want. 148 _, head := simpleCompound(form.Head, nil) 149 var args []string 150 for _, compound := range form.Args { 151 if compound.Begin() >= begin { 152 break 153 } 154 ok, arg := simpleCompound(compound, nil) 155 if ok { 156 // XXX Arguments that are not simple compounds are simply ignored. 157 args = append(args, arg) 158 } 159 } 160 161 words := make([]string, len(args)+2) 162 words[0] = head 163 words[len(words)-1] = current 164 copy(words[1:len(words)-1], args[:]) 165 166 cands, err := completeArg(words, ed) 167 if err != nil { 168 ed.notify("%v", err) 169 } 170 fixCandidates(cands, q) 171 return begin, end, cands 172 } 173 174 func findArgContext(n parse.Node) (int, int, string, parse.PrimaryType, *parse.Form) { 175 if sep, ok := n.(*parse.Sep); ok { 176 if form, ok := sep.Parent().(*parse.Form); ok { 177 return n.End(), n.End(), "", parse.Bareword, form 178 } 179 } 180 if primary, ok := n.(*parse.Primary); ok { 181 if compound, head := primaryInSimpleCompound(primary); compound != nil { 182 if form, ok := compound.Parent().(*parse.Form); ok { 183 if form.Head != compound { 184 return compound.Begin(), compound.End(), head, primary.Type, form 185 } 186 } 187 } 188 } 189 return -1, -1, "", 0, nil 190 } 191 192 // TODO: getStyle does redundant stats. 193 func complFilenameInner(head string, executableOnly bool) ([]*candidate, error) { 194 dir, fileprefix := path.Split(head) 195 if dir == "" { 196 dir = "." 197 } 198 199 infos, err := ioutil.ReadDir(dir) 200 if err != nil { 201 return nil, fmt.Errorf("cannot list directory %s: %v", dir, err) 202 } 203 204 cands := []*candidate{} 205 // Make candidates out of elements that match the file component. 206 for _, info := range infos { 207 name := info.Name() 208 // Irrevelant file. 209 if !strings.HasPrefix(name, fileprefix) { 210 continue 211 } 212 // Hide dot files unless file starts with a dot. 213 if !dotfile(fileprefix) && dotfile(name) { 214 continue 215 } 216 // Only accept searchable directories and executable files if 217 // executableOnly is true. 218 if executableOnly && !(info.IsDir() || (info.Mode()&0111) != 0) { 219 continue 220 } 221 222 // Full filename for source and getStyle. 223 full := head + name[len(fileprefix):] 224 225 suffix := " " 226 if info.IsDir() { 227 suffix = "/" 228 } else if info.Mode()&os.ModeSymlink != 0 { 229 stat, err := os.Stat(full) 230 if err == nil && stat.IsDir() { 231 // Symlink to directory. 232 suffix = "/" 233 } 234 } 235 236 cands = append(cands, &candidate{ 237 text: full, suffix: suffix, 238 display: styled{name, defaultLsColor.getStyle(full)}, 239 }) 240 } 241 242 return cands, nil 243 } 244 245 func fixCandidates(cands []*candidate, q parse.PrimaryType) []*candidate { 246 for _, cand := range cands { 247 quoted, _ := parse.QuoteAs(cand.text, q) 248 cand.text = quoted + cand.suffix 249 } 250 return cands 251 } 252 253 func dotfile(fname string) bool { 254 return strings.HasPrefix(fname, ".") 255 } 256 257 func complGetopt(ec *eval.EvalCtx, elemsv eval.IteratorValue, optsv eval.IteratorValue, argsv eval.IteratorValue) { 258 var ( 259 elems []string 260 opts []*getopt.Option 261 args []eval.FnValue 262 variadic bool 263 ) 264 // Convert arguments. 265 elemsv.Iterate(func(v eval.Value) bool { 266 elem, ok := v.(eval.String) 267 if !ok { 268 throwf("arg should be string, got %s", v.Kind()) 269 } 270 elems = append(elems, string(elem)) 271 return true 272 }) 273 optsv.Iterate(func(v eval.Value) bool { 274 m, ok := v.(eval.MapLike) 275 if !ok { 276 throwf("opt should be map-like, got %s", v.Kind()) 277 } 278 opt := &getopt.Option{} 279 vshort := maybeIndex(m, eval.String("short")) 280 if vshort != nil { 281 sv, ok := vshort.(eval.String) 282 if !ok { 283 throwf("short option should be string, got %s", vshort.Kind()) 284 } 285 s := string(sv) 286 r, size := utf8.DecodeRuneInString(s) 287 if r == utf8.RuneError || size != len(s) { 288 throwf("short option should be exactly one rune, got %v", parse.Quote(s)) 289 } 290 opt.Short = r 291 } 292 vlong := maybeIndex(m, eval.String("long")) 293 if vlong != nil { 294 s, ok := vlong.(eval.String) 295 if !ok { 296 throwf("long option should be string, got %s", vlong.Kind()) 297 } 298 opt.Long = string(s) 299 } 300 if vshort == nil && vlong == nil { 301 throwf("opt should have at least one of short and long as keys") 302 } 303 // TODO support &desc 304 opts = append(opts, opt) 305 return true 306 }) 307 argsv.Iterate(func(v eval.Value) bool { 308 sv, ok := v.(eval.String) 309 if ok { 310 if string(sv) == "..." { 311 variadic = true 312 return true 313 } 314 throwf("string except for ... not allowed as argument handler, got %s", parse.Quote(string(sv))) 315 } 316 arg, ok := v.(eval.FnValue) 317 if !ok { 318 throwf("argument handler should be fn, got %s", v.Kind()) 319 } 320 args = append(args, arg) 321 return true 322 }) 323 // TODO Configurable config 324 g := getopt.Getopt{opts, getopt.GNUGetoptLong} 325 _, _, ctx := g.Parse(elems) 326 out := ec.OutputChan() 327 _ = variadic // XXX 328 switch ctx.Type { 329 case getopt.NewOptionOrArgument, getopt.Argument: 330 case getopt.NewOption: 331 for _, opt := range opts { 332 if opt.Short != 0 { 333 out <- eval.String("-" + string(opt.Short)) 334 } 335 if opt.Long != "" { 336 out <- eval.String("--" + opt.Long) 337 } 338 } 339 case getopt.NewLongOption: 340 for _, opt := range opts { 341 if opt.Long != "" { 342 out <- eval.String("--" + opt.Long) 343 } 344 } 345 case getopt.LongOption: 346 for _, opt := range opts { 347 if strings.HasPrefix(opt.Long, ctx.Text) { 348 out <- eval.String("--" + opt.Long) 349 } 350 } 351 case getopt.ChainShortOption: 352 for _, opt := range opts { 353 if opt.Short != 0 { 354 // XXX loses chained options 355 out <- eval.String("-" + string(opt.Short)) 356 } 357 } 358 case getopt.OptionArgument: 359 } 360 } 361 362 func maybeIndex(m eval.MapLike, k eval.Value) eval.Value { 363 if !m.HasKey(k) { 364 return nil 365 } 366 return m.IndexOne(k) 367 }