src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/cli/modes/location.go (about)

     1  package modes
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"math"
     7  	"path/filepath"
     8  	"regexp"
     9  	"strings"
    10  
    11  	"src.elv.sh/pkg/cli"
    12  	"src.elv.sh/pkg/cli/tk"
    13  	"src.elv.sh/pkg/fsutil"
    14  	"src.elv.sh/pkg/store/storedefs"
    15  	"src.elv.sh/pkg/ui"
    16  )
    17  
    18  // Location is a mode for viewing location history and changing to a selected
    19  // directory. It is based on the ComboBox widget.
    20  type Location interface {
    21  	tk.ComboBox
    22  }
    23  
    24  // LocationSpec is the configuration to start the location history feature.
    25  type LocationSpec struct {
    26  	// Key bindings.
    27  	Bindings tk.Bindings
    28  	// Store provides the directory history and the function to change directory.
    29  	Store LocationStore
    30  	// IteratePinned specifies pinned directories by calling the given function
    31  	// with all pinned directories.
    32  	IteratePinned func(func(string))
    33  	// IterateHidden specifies hidden directories by calling the given function
    34  	// with all hidden directories.
    35  	IterateHidden func(func(string))
    36  	// IterateWorksapce specifies workspace configuration.
    37  	IterateWorkspaces LocationWSIterator
    38  	// Configuration for the filter.
    39  	Filter FilterSpec
    40  }
    41  
    42  // LocationStore defines the interface for interacting with the directory history.
    43  type LocationStore interface {
    44  	Dirs(blacklist map[string]struct{}) ([]storedefs.Dir, error)
    45  	Chdir(dir string) error
    46  	Getwd() (string, error)
    47  }
    48  
    49  // A special score for pinned directories.
    50  var pinnedScore = math.Inf(1)
    51  
    52  var errNoDirectoryHistoryStore = errors.New("no directory history store")
    53  
    54  // NewLocation creates a new location mode.
    55  func NewLocation(app cli.App, cfg LocationSpec) (Location, error) {
    56  	if cfg.Store == nil {
    57  		return nil, errNoDirectoryHistoryStore
    58  	}
    59  
    60  	dirs := []storedefs.Dir{}
    61  	blacklist := map[string]struct{}{}
    62  	wsKind, wsRoot := "", ""
    63  
    64  	if cfg.IteratePinned != nil {
    65  		cfg.IteratePinned(func(s string) {
    66  			blacklist[s] = struct{}{}
    67  			dirs = append(dirs, storedefs.Dir{Score: pinnedScore, Path: s})
    68  		})
    69  	}
    70  	if cfg.IterateHidden != nil {
    71  		cfg.IterateHidden(func(s string) { blacklist[s] = struct{}{} })
    72  	}
    73  	wd, err := cfg.Store.Getwd()
    74  	if err == nil {
    75  		blacklist[wd] = struct{}{}
    76  		if cfg.IterateWorkspaces != nil {
    77  			wsKind, wsRoot = cfg.IterateWorkspaces.Parse(wd)
    78  		}
    79  	}
    80  	storedDirs, err := cfg.Store.Dirs(blacklist)
    81  	if err != nil {
    82  		return nil, fmt.Errorf("db error: %v", err)
    83  	}
    84  	for _, dir := range storedDirs {
    85  		if filepath.IsAbs(dir.Path) {
    86  			dirs = append(dirs, dir)
    87  		} else if wsKind != "" && hasPathPrefix(dir.Path, wsKind) {
    88  			dirs = append(dirs, dir)
    89  		}
    90  	}
    91  
    92  	l := locationList{dirs}
    93  
    94  	w := tk.NewComboBox(tk.ComboBoxSpec{
    95  		CodeArea: tk.CodeAreaSpec{
    96  			Prompt:      modePrompt(" LOCATION ", true),
    97  			Highlighter: cfg.Filter.Highlighter,
    98  		},
    99  		ListBox: tk.ListBoxSpec{
   100  			Bindings: cfg.Bindings,
   101  			OnAccept: func(it tk.Items, i int) {
   102  				path := it.(locationList).dirs[i].Path
   103  				if strings.HasPrefix(path, wsKind) {
   104  					path = wsRoot + path[len(wsKind):]
   105  				}
   106  				err := cfg.Store.Chdir(path)
   107  				if err != nil {
   108  					app.Notify(ErrorText(err))
   109  				}
   110  				app.PopAddon()
   111  			},
   112  		},
   113  		OnFilter: func(w tk.ComboBox, p string) {
   114  			w.ListBox().Reset(l.filter(cfg.Filter.makePredicate(p)), 0)
   115  		},
   116  	})
   117  	return w, nil
   118  }
   119  
   120  func hasPathPrefix(path, prefix string) bool {
   121  	return path == prefix ||
   122  		strings.HasPrefix(path, prefix+string(filepath.Separator))
   123  }
   124  
   125  // LocationWSIterator is a function that iterates all workspaces by calling
   126  // the passed function with the name and pattern of each kind of workspace.
   127  // Iteration should stop when the called function returns false.
   128  type LocationWSIterator func(func(kind, pattern string) bool)
   129  
   130  // Parse returns whether the path matches any kind of workspace. If there is
   131  // a match, it returns the kind of the workspace and the root. It there is no
   132  // match, it returns "", "".
   133  func (ws LocationWSIterator) Parse(path string) (kind, root string) {
   134  	var foundKind, foundRoot string
   135  	ws(func(kind, pattern string) bool {
   136  		if !strings.HasPrefix(pattern, "^") {
   137  			pattern = "^" + pattern
   138  		}
   139  		re, err := regexp.Compile(pattern)
   140  		if err != nil {
   141  			// TODO(xiaq): Surface the error.
   142  			return true
   143  		}
   144  		if root := re.FindString(path); root != "" {
   145  			foundKind, foundRoot = kind, root
   146  			return false
   147  		}
   148  		return true
   149  	})
   150  	return foundKind, foundRoot
   151  }
   152  
   153  type locationList struct {
   154  	dirs []storedefs.Dir
   155  }
   156  
   157  func (l locationList) filter(p func(string) bool) locationList {
   158  	var filteredDirs []storedefs.Dir
   159  	for _, dir := range l.dirs {
   160  		if p(fsutil.TildeAbbr(dir.Path)) {
   161  			filteredDirs = append(filteredDirs, dir)
   162  		}
   163  	}
   164  	return locationList{filteredDirs}
   165  }
   166  
   167  func (l locationList) Show(i int) ui.Text {
   168  	return ui.T(fmt.Sprintf("%s %s",
   169  		showScore(l.dirs[i].Score), fsutil.TildeAbbr(l.dirs[i].Path)))
   170  }
   171  
   172  func (l locationList) Len() int { return len(l.dirs) }
   173  
   174  func showScore(f float64) string {
   175  	if f == pinnedScore {
   176  		return "  *"
   177  	}
   178  	return fmt.Sprintf("%3.0f", f)
   179  }