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  }