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  }