github.com/elves/elvish@v0.15.0/pkg/cli/addons/location/location.go (about) 1 // Package location implements an addon that supports viewing location history 2 // and changing to a selected directory. 3 package location 4 5 import ( 6 "fmt" 7 "math" 8 "os" 9 "path/filepath" 10 "regexp" 11 "strings" 12 13 "github.com/elves/elvish/pkg/cli" 14 "github.com/elves/elvish/pkg/fsutil" 15 "github.com/elves/elvish/pkg/store" 16 "github.com/elves/elvish/pkg/ui" 17 ) 18 19 // Config is the configuration to start the location history feature. 20 type Config struct { 21 // Binding is the key binding. 22 Binding cli.Handler 23 // Store provides the directory history and the function to change directory. 24 Store Store 25 // IteratePinned specifies pinned directories by calling the given function 26 // with all pinned directories. 27 IteratePinned func(func(string)) 28 // IterateHidden specifies hidden directories by calling the given function 29 // with all hidden directories. 30 IterateHidden func(func(string)) 31 // IterateWorksapce specifies workspace configuration. 32 IterateWorkspaces WorkspaceIterator 33 } 34 35 // Store defines the interface for interacting with the directory history. 36 type Store interface { 37 Dirs(blacklist map[string]struct{}) ([]store.Dir, error) 38 Chdir(dir string) error 39 Getwd() (string, error) 40 } 41 42 // A special score for pinned directories. 43 var pinnedScore = math.Inf(1) 44 45 // Start starts the directory history feature. 46 func Start(app cli.App, cfg Config) { 47 if cfg.Store == nil { 48 app.Notify("no dir history store") 49 return 50 } 51 52 dirs := []store.Dir{} 53 blacklist := map[string]struct{}{} 54 wsKind, wsRoot := "", "" 55 56 if cfg.IteratePinned != nil { 57 cfg.IteratePinned(func(s string) { 58 blacklist[s] = struct{}{} 59 dirs = append(dirs, store.Dir{Score: pinnedScore, Path: s}) 60 }) 61 } 62 if cfg.IterateHidden != nil { 63 cfg.IterateHidden(func(s string) { blacklist[s] = struct{}{} }) 64 } 65 wd, err := cfg.Store.Getwd() 66 if err == nil { 67 blacklist[wd] = struct{}{} 68 if cfg.IterateWorkspaces != nil { 69 wsKind, wsRoot = cfg.IterateWorkspaces.Parse(wd) 70 } 71 } 72 storedDirs, err := cfg.Store.Dirs(blacklist) 73 if err != nil { 74 app.Notify("db error: " + err.Error()) 75 if len(dirs) == 0 { 76 return 77 } 78 } 79 for _, dir := range storedDirs { 80 if filepath.IsAbs(dir.Path) { 81 dirs = append(dirs, dir) 82 } else if wsKind != "" && hasPathPrefix(dir.Path, wsKind) { 83 dirs = append(dirs, dir) 84 } 85 } 86 87 l := list{dirs} 88 89 w := cli.NewComboBox(cli.ComboBoxSpec{ 90 CodeArea: cli.CodeAreaSpec{ 91 Prompt: cli.ModePrompt(" LOCATION ", true), 92 }, 93 ListBox: cli.ListBoxSpec{ 94 OverlayHandler: cfg.Binding, 95 OnAccept: func(it cli.Items, i int) { 96 path := it.(list).dirs[i].Path 97 if strings.HasPrefix(path, wsKind) { 98 path = wsRoot + path[len(wsKind):] 99 } 100 err := cfg.Store.Chdir(path) 101 if err != nil { 102 app.Notify(err.Error()) 103 } 104 app.MutateState(func(s *cli.State) { s.Addon = nil }) 105 }, 106 }, 107 OnFilter: func(w cli.ComboBox, p string) { 108 w.ListBox().Reset(l.filter(p), 0) 109 }, 110 }) 111 app.MutateState(func(s *cli.State) { s.Addon = w }) 112 app.Redraw() 113 } 114 115 func hasPathPrefix(path, prefix string) bool { 116 return path == prefix || 117 strings.HasPrefix(path, prefix+string(filepath.Separator)) 118 } 119 120 // WorkspaceIterator is a function that iterates all workspaces by calling 121 // the passed function with the name and pattern of each kind of workspace. 122 // Iteration should stop when the called function returns false. 123 type WorkspaceIterator func(func(kind, pattern string) bool) 124 125 // Parse returns whether the path matches any kind of workspace. If there is 126 // a match, it returns the kind of the workspace and the root. It there is no 127 // match, it returns "", "". 128 func (ws WorkspaceIterator) Parse(path string) (kind, root string) { 129 var foundKind, foundRoot string 130 ws(func(kind, pattern string) bool { 131 if !strings.HasPrefix(pattern, "^") { 132 pattern = "^" + pattern 133 } 134 re, err := regexp.Compile(pattern) 135 if err != nil { 136 // TODO(xiaq): Surface the error. 137 return true 138 } 139 if root := re.FindString(path); root != "" { 140 foundKind, foundRoot = kind, root 141 return false 142 } 143 return true 144 }) 145 return foundKind, foundRoot 146 } 147 148 type list struct { 149 dirs []store.Dir 150 } 151 152 func (l list) filter(p string) list { 153 if p == "" { 154 return l 155 } 156 re := makeRegexpForPattern(p) 157 var filteredDirs []store.Dir 158 for _, dir := range l.dirs { 159 if re.MatchString(fsutil.TildeAbbr(dir.Path)) { 160 filteredDirs = append(filteredDirs, dir) 161 } 162 } 163 return list{filteredDirs} 164 } 165 166 var ( 167 quotedPathSep = regexp.QuoteMeta(string(os.PathSeparator)) 168 emptyRe = regexp.MustCompile("") 169 ) 170 171 func makeRegexpForPattern(p string) *regexp.Regexp { 172 var b strings.Builder 173 b.WriteString("(?i).*") // Ignore case, unanchored 174 for i, seg := range strings.Split(p, string(os.PathSeparator)) { 175 if i > 0 { 176 b.WriteString(".*" + quotedPathSep + ".*") 177 } 178 b.WriteString(regexp.QuoteMeta(seg)) 179 } 180 b.WriteString(".*") 181 re, err := regexp.Compile(b.String()) 182 if err != nil { 183 // TODO: Log the error. 184 return emptyRe 185 } 186 return re 187 } 188 189 func (l list) Show(i int) ui.Text { 190 return ui.T(fmt.Sprintf("%s %s", 191 showScore(l.dirs[i].Score), fsutil.TildeAbbr(l.dirs[i].Path))) 192 } 193 194 func (l list) Len() int { return len(l.dirs) } 195 196 func showScore(f float64) string { 197 if f == pinnedScore { 198 return " *" 199 } 200 return fmt.Sprintf("%3.0f", f) 201 }