github.com/xyproto/u-root@v6.0.1-0.20200302025726-5528e0c77a3c+incompatible/cmds/core/elvish/edit/location/location.go (about) 1 // Package location implements the location mode for the editor. 2 package location 3 4 import ( 5 "bytes" 6 "fmt" 7 "math" 8 "os" 9 "path/filepath" 10 "regexp" 11 "strings" 12 13 "github.com/u-root/u-root/cmds/core/elvish/edit/eddefs" 14 "github.com/u-root/u-root/cmds/core/elvish/edit/ui" 15 "github.com/u-root/u-root/cmds/core/elvish/eval" 16 "github.com/u-root/u-root/cmds/core/elvish/eval/vals" 17 "github.com/u-root/u-root/cmds/core/elvish/eval/vars" 18 "github.com/u-root/u-root/cmds/core/elvish/hashmap" 19 "github.com/u-root/u-root/cmds/core/elvish/parse" 20 "github.com/u-root/u-root/cmds/core/elvish/store/storedefs" 21 "github.com/u-root/u-root/cmds/core/elvish/util" 22 "github.com/u-root/u-root/cmds/core/elvish/vector" 23 ) 24 25 var logger = util.GetLogger("[edit/location] ") 26 27 // pinnedScore is a special value of Score in storedefs.Dir to represent that the 28 // directory is pinned. 29 var pinnedScore = math.Inf(1) 30 31 type mode struct { 32 editor eddefs.Editor 33 binding eddefs.BindingMap 34 hidden vector.Vector 35 pinned vector.Vector 36 workspaces hashmap.Map 37 matcher eval.Callable 38 } 39 40 var matchDirPatternBuiltin = eval.NewBuiltinFn("edit:location:match-dir-pattern", matchDirPattern) 41 42 // Init initializes the location mode for an Editor. 43 func Init(ed eddefs.Editor, ns eval.Ns) { 44 m := &mode{ed, eddefs.EmptyBindingMap, 45 vals.EmptyList, vals.EmptyList, vals.EmptyMap, matchDirPatternBuiltin} 46 47 ns.AddNs("location", 48 eval.Ns{ 49 "binding": vars.FromPtr(&m.binding), 50 "hidden": vars.FromPtr(&m.hidden), 51 "pinned": vars.FromPtr(&m.pinned), 52 "matcher": vars.FromPtr(&m.matcher), 53 "workspaces": vars.FromPtr(&m.workspaces), 54 }.AddBuiltinFn("edit:location:", "start", m.start). 55 AddFn("match-dir-pattern", matchDirPatternBuiltin)) 56 57 /* 58 ed.Evaler().AddAfterChdir(func(string) { 59 store := ed.Daemon() 60 if store == nil { 61 return 62 } 63 pwd, err := os.Getwd() 64 if err != nil { 65 logger.Println("Failed to get pwd in after-chdir hook:", err) 66 } 67 go addDir(store, pwd, m.workspaces) 68 }) 69 */ 70 } 71 72 func addDir(store storedefs.Store, pwd string, workspaces hashmap.Map) { 73 _, err := store.Add(pwd) 74 if err != nil { 75 logger.Println("add dir in after-chdir hook:", err) 76 return 77 } 78 ws := matchWorkspace(pwd, workspaces) 79 if ws == nil { 80 return 81 } 82 _, err = store.Add(ws.workspacify(pwd)) 83 if err != nil { 84 logger.Println("add workspacified dir in after-chdir hook:", err) 85 } 86 } 87 88 type wsInfo struct { 89 name string 90 root string 91 } 92 93 func matchWorkspace(dir string, workspaces hashmap.Map) *wsInfo { 94 for it := workspaces.Iterator(); it.HasElem(); it.Next() { 95 k, v := it.Elem() 96 name, ok := k.(string) 97 if !ok { 98 // TODO: Surface to user 99 logger.Println("$workspaces key not string", k) 100 continue 101 } 102 if strings.HasPrefix(name, "/") { 103 // TODO: Surface to user 104 logger.Println("$workspaces key starts with /", k) 105 continue 106 } 107 pattern, ok := v.(string) 108 if !ok { 109 // TODO: Surface to user 110 logger.Println("$workspaces value not string", v) 111 continue 112 } 113 if !strings.HasPrefix(pattern, "^") { 114 pattern = "^" + pattern 115 } 116 re, err := regexp.Compile(pattern) 117 if err != nil { 118 // TODO: Surface to user 119 logger.Println("$workspaces pattern invalid", pattern) 120 continue 121 } 122 if ws := re.FindString(dir); ws != "" { 123 return &wsInfo{name, ws} 124 } 125 } 126 return nil 127 } 128 129 func (w *wsInfo) workspacify(dir string) string { 130 return w.name + dir[len(w.root):] 131 } 132 133 func (w *wsInfo) unworkspacify(dir string) string { 134 return w.root + dir[len(w.name):] 135 } 136 137 func (m *mode) start() { 138 ed := m.editor 139 140 ed.Notify("store offline, cannot start location mode") 141 return 142 143 /* what a fucking mess this is. 144 145 // Pinned directories are also blacklisted to prevent them from showing up 146 // twice. 147 black := convertListsToSet(m.hidden, m.pinned) 148 pwd, err := os.Getwd() 149 if err == nil { 150 black[pwd] = struct{}{} 151 } 152 stored, err := daemon.Dirs(black) 153 if err != nil { 154 ed.Notify("store error: %v", err) 155 return 156 } 157 158 // TODO: Move workspace filtering to the daemon. 159 ws := matchWorkspace(pwd, m.workspaces) 160 wsName := "" 161 if ws != nil { 162 wsName = ws.name 163 } 164 wsNameSlash := wsName + string(filepath.Separator) 165 166 var filtered []storedefs.Dir 167 for _, dir := range stored { 168 if filepath.IsAbs(dir.Path) || (wsName != "" && (dir.Path == wsName || strings.HasPrefix(dir.Path, wsNameSlash))) { 169 filtered = append(filtered, dir) 170 } 171 } 172 173 // Prepend pinned dirs. 174 pinnedDirs := convertListToDirs(m.pinned) 175 dirs := make([]storedefs.Dir, len(pinnedDirs)+len(filtered)) 176 copy(dirs, pinnedDirs) 177 copy(dirs[len(pinnedDirs):], filtered) 178 179 // Drop the error. When there is an error, home is "", which is used to 180 // signify "no home known" in location. 181 home, _ := util.GetHome("") 182 ed.SetModeListing(m.binding, 183 newProvider(dirs, home, ws, ed.Evaler(), m.matcher)) 184 */ 185 } 186 187 // convertListToDirs converts a list of strings to []storedefs.Dir. It uses the 188 // special score of pinnedScore to signify that the directory is pinned. 189 func convertListToDirs(li vector.Vector) []storedefs.Dir { 190 pinned := make([]storedefs.Dir, 0, li.Len()) 191 // XXX(xiaq): silently drops non-string items. 192 for it := li.Iterator(); it.HasElem(); it.Next() { 193 if s, ok := it.Elem().(string); ok { 194 pinned = append(pinned, storedefs.Dir{s, pinnedScore}) 195 } 196 } 197 return pinned 198 } 199 200 func convertListsToSet(lis ...vector.Vector) map[string]struct{} { 201 set := make(map[string]struct{}) 202 // XXX(xiaq): silently drops non-string items. 203 for _, li := range lis { 204 for it := li.Iterator(); it.HasElem(); it.Next() { 205 if s, ok := it.Elem().(string); ok { 206 set[s] = struct{}{} 207 } 208 } 209 } 210 return set 211 } 212 213 type provider struct { 214 all []storedefs.Dir 215 filtered []storedefs.Dir 216 home string // The home directory; leave empty if unknown. 217 ws *wsInfo 218 ev *eval.Evaler 219 matcher eval.Callable 220 } 221 222 func newProvider(dirs []storedefs.Dir, home string, ws *wsInfo, ev *eval.Evaler, matcher eval.Callable) *provider { 223 return &provider{dirs, nil, home, ws, ev, matcher} 224 } 225 226 func (*provider) ModeTitle(i int) string { 227 return " LOCATION " 228 } 229 230 func (*provider) CursorOnModeLine() bool { 231 return true 232 } 233 234 func (p *provider) Len() int { 235 return len(p.filtered) 236 } 237 238 func (p *provider) Show(i int) (string, ui.Styled) { 239 var header string 240 score := p.filtered[i].Score 241 if score == pinnedScore { 242 header = "*" 243 } else { 244 header = fmt.Sprintf("%.0f", score) 245 } 246 return header, ui.Unstyled(showPath(p.filtered[i].Path, p.home)) 247 } 248 249 func showPath(path, home string) string { 250 if home != "" && path == home { 251 return "~" 252 } else if home != "" && strings.HasPrefix(path, home+"/") { 253 return "~/" + parse.Quote(path[len(home)+1:]) 254 } else { 255 return parse.Quote(path) 256 } 257 } 258 259 func (p *provider) Filter(filter string) int { 260 p.filtered = nil 261 262 // TODO: this is just a replica of `filterRawCandidates`. 263 matcherInput := make(chan interface{}, len(p.all)) 264 stopCollector := make(chan struct{}) 265 go func() { 266 defer close(matcherInput) 267 for _, item := range p.all { 268 select { 269 case matcherInput <- showPath(item.Path, p.home): 270 logger.Printf("put %s\n", item.Path) 271 case <-stopCollector: 272 return 273 } 274 } 275 }() 276 defer close(stopCollector) 277 278 ports := []*eval.Port{ 279 {Chan: matcherInput, File: eval.DevNull}, {File: os.Stdout}, {File: os.Stderr}} 280 ec := eval.NewTopFrame(p.ev, eval.NewInternalSource("[editor matcher]"), ports) 281 args := []interface{}{filter} 282 283 values, err := ec.CaptureOutput(p.matcher, args, eval.NoOpts) 284 if err != nil { 285 logger.Printf("failed to match %s: %v", filter, err) 286 return -1 287 } else if got, expect := len(values), len(p.all); got != expect { 288 logger.Printf("wrong match count: got %d, want %d", got, expect) 289 return -1 290 } 291 292 for i, value := range values { 293 if vals.Bool(value) { 294 p.filtered = append(p.filtered, p.all[i]) 295 } 296 } 297 298 if len(p.filtered) == 0 { 299 return -1 300 } 301 return 0 302 } 303 304 var emptyRegexp = regexp.MustCompile("") 305 306 func makeLocationFilterPattern(s string, ignoreCase bool) *regexp.Regexp { 307 var b bytes.Buffer 308 if ignoreCase { 309 b.WriteString("(?i)") 310 } 311 b.WriteString(".*") 312 segs := strings.Split(s, "/") 313 for i, seg := range segs { 314 if i > 0 { 315 b.WriteString(".*/.*") 316 } 317 b.WriteString(regexp.QuoteMeta(seg)) 318 } 319 b.WriteString(".*") 320 p, err := regexp.Compile(b.String()) 321 if err != nil { 322 logger.Printf("failed to compile regexp %q: %v", b.String(), err) 323 return emptyRegexp 324 } 325 return p 326 } 327 328 func (p *provider) Accept(i int, ed eddefs.Editor) { 329 path := p.filtered[i].Path 330 if !filepath.IsAbs(path) { 331 path = p.ws.unworkspacify(path) 332 } 333 err := ed.Evaler().Chdir(path) 334 if err != nil { 335 ed.Notify("%v", err) 336 } 337 ed.SetModeInsert() 338 } 339 340 func matchDirPattern(fm *eval.Frame, opts eval.RawOptions, pattern string, inputs eval.Inputs) { 341 var options struct { 342 IgnoreCase bool 343 } 344 opts.Scan(&options) 345 346 p := makeLocationFilterPattern(pattern, options.IgnoreCase) 347 out := fm.OutputChan() 348 inputs(func(v interface{}) { 349 s, ok := v.(string) 350 if !ok { 351 logger.Printf("input item must be string, but got %#v", v) 352 return 353 } 354 out <- vals.Bool(p.MatchString(s)) 355 }) 356 }