github.com/u-root/u-root@v7.0.1-0.20200915234505-ad7babab0a8e+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 144 // convertListToDirs converts a list of strings to []storedefs.Dir. It uses the 145 // special score of pinnedScore to signify that the directory is pinned. 146 func convertListToDirs(li vector.Vector) []storedefs.Dir { 147 pinned := make([]storedefs.Dir, 0, li.Len()) 148 // XXX(xiaq): silently drops non-string items. 149 for it := li.Iterator(); it.HasElem(); it.Next() { 150 if s, ok := it.Elem().(string); ok { 151 pinned = append(pinned, storedefs.Dir{s, pinnedScore}) 152 } 153 } 154 return pinned 155 } 156 157 func convertListsToSet(lis ...vector.Vector) map[string]struct{} { 158 set := make(map[string]struct{}) 159 // XXX(xiaq): silently drops non-string items. 160 for _, li := range lis { 161 for it := li.Iterator(); it.HasElem(); it.Next() { 162 if s, ok := it.Elem().(string); ok { 163 set[s] = struct{}{} 164 } 165 } 166 } 167 return set 168 } 169 170 type provider struct { 171 all []storedefs.Dir 172 filtered []storedefs.Dir 173 home string // The home directory; leave empty if unknown. 174 ws *wsInfo 175 ev *eval.Evaler 176 matcher eval.Callable 177 } 178 179 func newProvider(dirs []storedefs.Dir, home string, ws *wsInfo, ev *eval.Evaler, matcher eval.Callable) *provider { 180 return &provider{dirs, nil, home, ws, ev, matcher} 181 } 182 183 func (*provider) ModeTitle(i int) string { 184 return " LOCATION " 185 } 186 187 func (*provider) CursorOnModeLine() bool { 188 return true 189 } 190 191 func (p *provider) Len() int { 192 return len(p.filtered) 193 } 194 195 func (p *provider) Show(i int) (string, ui.Styled) { 196 var header string 197 score := p.filtered[i].Score 198 if score == pinnedScore { 199 header = "*" 200 } else { 201 header = fmt.Sprintf("%.0f", score) 202 } 203 return header, ui.Unstyled(showPath(p.filtered[i].Path, p.home)) 204 } 205 206 func showPath(path, home string) string { 207 if home != "" && path == home { 208 return "~" 209 } else if home != "" && strings.HasPrefix(path, home+"/") { 210 return "~/" + parse.Quote(path[len(home)+1:]) 211 } else { 212 return parse.Quote(path) 213 } 214 } 215 216 func (p *provider) Filter(filter string) int { 217 p.filtered = nil 218 219 // TODO: this is just a replica of `filterRawCandidates`. 220 matcherInput := make(chan interface{}, len(p.all)) 221 stopCollector := make(chan struct{}) 222 go func() { 223 defer close(matcherInput) 224 for _, item := range p.all { 225 select { 226 case matcherInput <- showPath(item.Path, p.home): 227 logger.Printf("put %s\n", item.Path) 228 case <-stopCollector: 229 return 230 } 231 } 232 }() 233 defer close(stopCollector) 234 235 ports := []*eval.Port{ 236 {Chan: matcherInput, File: eval.DevNull}, {File: os.Stdout}, {File: os.Stderr}} 237 ec := eval.NewTopFrame(p.ev, eval.NewInternalSource("[editor matcher]"), ports) 238 args := []interface{}{filter} 239 240 values, err := ec.CaptureOutput(p.matcher, args, eval.NoOpts) 241 if err != nil { 242 logger.Printf("failed to match %s: %v", filter, err) 243 return -1 244 } else if got, expect := len(values), len(p.all); got != expect { 245 logger.Printf("wrong match count: got %d, want %d", got, expect) 246 return -1 247 } 248 249 for i, value := range values { 250 if vals.Bool(value) { 251 p.filtered = append(p.filtered, p.all[i]) 252 } 253 } 254 255 if len(p.filtered) == 0 { 256 return -1 257 } 258 return 0 259 } 260 261 var emptyRegexp = regexp.MustCompile("") 262 263 func makeLocationFilterPattern(s string, ignoreCase bool) *regexp.Regexp { 264 var b bytes.Buffer 265 if ignoreCase { 266 b.WriteString("(?i)") 267 } 268 b.WriteString(".*") 269 segs := strings.Split(s, "/") 270 for i, seg := range segs { 271 if i > 0 { 272 b.WriteString(".*/.*") 273 } 274 b.WriteString(regexp.QuoteMeta(seg)) 275 } 276 b.WriteString(".*") 277 p, err := regexp.Compile(b.String()) 278 if err != nil { 279 logger.Printf("failed to compile regexp %q: %v", b.String(), err) 280 return emptyRegexp 281 } 282 return p 283 } 284 285 func (p *provider) Accept(i int, ed eddefs.Editor) { 286 path := p.filtered[i].Path 287 if !filepath.IsAbs(path) { 288 path = p.ws.unworkspacify(path) 289 } 290 err := ed.Evaler().Chdir(path) 291 if err != nil { 292 ed.Notify("%v", err) 293 } 294 ed.SetModeInsert() 295 } 296 297 func matchDirPattern(fm *eval.Frame, opts eval.RawOptions, pattern string, inputs eval.Inputs) { 298 var options struct { 299 IgnoreCase bool 300 } 301 opts.Scan(&options) 302 303 p := makeLocationFilterPattern(pattern, options.IgnoreCase) 304 out := fm.OutputChan() 305 inputs(func(v interface{}) { 306 s, ok := v.(string) 307 if !ok { 308 logger.Printf("input item must be string, but got %#v", v) 309 return 310 } 311 out <- vals.Bool(p.MatchString(s)) 312 }) 313 }