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  }