github.com/mem/u-root@v2.0.1-0.20181004165302-9b18b4636a33+incompatible/cmds/elvish/edit/navigation.go (about)

     1  package edit
     2  
     3  import (
     4  	"errors"
     5  	"os"
     6  	"path"
     7  	"sort"
     8  	"strings"
     9  	"unicode/utf8"
    10  
    11  	"github.com/u-root/u-root/cmds/elvish/edit/eddefs"
    12  	"github.com/u-root/u-root/cmds/elvish/edit/lscolors"
    13  	"github.com/u-root/u-root/cmds/elvish/edit/ui"
    14  	"github.com/u-root/u-root/cmds/elvish/eval"
    15  	"github.com/u-root/u-root/cmds/elvish/eval/vars"
    16  	"github.com/u-root/u-root/cmds/elvish/parse"
    17  	"github.com/u-root/u-root/cmds/elvish/util"
    18  )
    19  
    20  // Navigation subsystem.
    21  
    22  // Interface.
    23  
    24  type navigation struct {
    25  	binding eddefs.BindingMap
    26  	chdir   func(string) error
    27  	navigationState
    28  }
    29  
    30  type navigationState struct {
    31  	current    *navColumn
    32  	parent     *navColumn
    33  	preview    navPreview
    34  	showHidden bool
    35  	filtering  bool
    36  	filter     string
    37  }
    38  
    39  func init() { atEditorInit(initNavigation) }
    40  
    41  func initNavigation(ed *editor, ns eval.Ns) {
    42  	n := &navigation{
    43  		binding: emptyBindingMap,
    44  		chdir:   ed.Evaler().Chdir,
    45  	}
    46  	ed.navigation = n
    47  
    48  	subns := eval.Ns{
    49  		"binding": vars.FromPtr(&n.binding),
    50  	}
    51  	subns.AddBuiltinFns("edit:navigation:", map[string]interface{}{
    52  		"start":                    func() { n.start(ed) },
    53  		"up":                       n.prev,
    54  		"down":                     n.next,
    55  		"page-up":                  n.pageUp,
    56  		"page-down":                n.pageDown,
    57  		"left":                     n.ascend,
    58  		"right":                    n.descend,
    59  		"file-preview-up":          n.filePreviewUp,
    60  		"file-preview-down":        n.filePreviewDown,
    61  		"trigger-shown-hidden":     n.triggerShowHidden,
    62  		"trigger-filter":           n.triggerFilter,
    63  		"insert-selected":          func() { n.insertSelected(ed) },
    64  		"insert-selected-and-quit": func() { n.insertSelectedAndQuit(ed) },
    65  		"default":                  func() { n.defaultFn(ed) },
    66  	})
    67  	ns.AddNs("navigation", subns)
    68  }
    69  
    70  type navPreview interface {
    71  	FullWidth(int) int
    72  	List(int) ui.Renderer
    73  }
    74  
    75  func (n *navigation) Teardown() {
    76  	n.navigationState = navigationState{}
    77  }
    78  
    79  func (n *navigation) Binding(k ui.Key) eval.Callable {
    80  	return n.binding.GetOrDefault(k)
    81  }
    82  
    83  func (n *navigation) ModeLine() ui.Renderer {
    84  	title := " NAVIGATING "
    85  	if n.showHidden {
    86  		title += "(show hidden) "
    87  	}
    88  	return ui.NewModeLineRenderer(title, n.filter)
    89  }
    90  
    91  func (n *navigation) CursorOnModeLine() bool {
    92  	return n.filtering
    93  }
    94  
    95  func (n *navigation) start(ed *editor) {
    96  	n.refresh()
    97  	ed.SetMode(n)
    98  }
    99  
   100  func (n *navigation) pageUp() {
   101  	n.current.pageUp()
   102  	n.refresh()
   103  }
   104  
   105  func (n *navigation) pageDown() {
   106  	n.current.pageDown()
   107  	n.refresh()
   108  }
   109  
   110  func (n *navigation) filePreviewUp() {
   111  	fp, ok := n.preview.(*navFilePreview)
   112  	if ok {
   113  		if fp.beginLine > 0 {
   114  			fp.beginLine--
   115  		}
   116  	}
   117  }
   118  
   119  func (n *navigation) filePreviewDown() {
   120  	fp, ok := n.preview.(*navFilePreview)
   121  	if ok {
   122  		if fp.beginLine < len(fp.lines)-1 {
   123  			fp.beginLine++
   124  		}
   125  	}
   126  }
   127  
   128  func (n *navigation) triggerShowHidden() {
   129  	n.showHidden = !n.showHidden
   130  	n.refresh()
   131  }
   132  
   133  func (n *navigation) triggerFilter() {
   134  	n.filtering = !n.filtering
   135  }
   136  
   137  func (n *navigation) insertSelected(ed *editor) {
   138  	ed.InsertAtDot(parse.Quote(n.current.selectedName()) + " ")
   139  }
   140  
   141  func (n *navigation) insertSelectedAndQuit(ed *editor) {
   142  	ed.InsertAtDot(parse.Quote(n.current.selectedName()) + " ")
   143  	ed.SetModeInsert()
   144  }
   145  
   146  func (n *navigation) defaultFn(ed *editor) {
   147  	// Use key binding for insert mode without exiting nigation mode.
   148  	k := ed.lastKey
   149  	if n.filtering && likeChar(k) {
   150  		n.filter += k.String()
   151  		n.refreshCurrent()
   152  		n.refreshDirPreview()
   153  	} else if n.filtering && k == (ui.Key{ui.Backspace, 0}) {
   154  		_, size := utf8.DecodeLastRuneInString(n.filter)
   155  		if size > 0 {
   156  			n.filter = n.filter[:len(n.filter)-size]
   157  			n.refreshCurrent()
   158  			n.refreshDirPreview()
   159  		}
   160  	} else {
   161  		fn := ed.insert.binding.GetOrDefault(k)
   162  		if fn == nil {
   163  			ed.Notify("key %s unbound and no default binding", k)
   164  		} else {
   165  			ed.CallFn(fn)
   166  		}
   167  	}
   168  }
   169  
   170  // Implementation.
   171  // TODO(xiaq): Remember which file was selected in each directory.
   172  
   173  var errorEmptyCwd = errors.New("current directory is empty")
   174  
   175  func (n *navigation) maintainSelected(name string) {
   176  	n.current.selected = 0
   177  	for i, s := range n.current.candidates {
   178  		if s.Text > name {
   179  			break
   180  		}
   181  		n.current.selected = i
   182  	}
   183  }
   184  
   185  func (n *navigation) refreshCurrent() {
   186  	selectedName := n.current.selectedName()
   187  	all, err := n.loaddir(".")
   188  	if err != nil {
   189  		n.current = newErrNavColumn(err)
   190  		return
   191  	}
   192  	// Try to select the old selected file.
   193  	// XXX(xiaq): This would break when we support alternative ordering.
   194  	n.current = newNavColumn(all, func(i int) bool {
   195  		return i == 0 || all[i].Text <= selectedName
   196  	})
   197  	n.current.changeFilter(n.filter)
   198  	n.maintainSelected(selectedName)
   199  }
   200  
   201  func (n *navigation) refreshParent() {
   202  	wd, err := os.Getwd()
   203  	if err != nil {
   204  		n.parent = newErrNavColumn(err)
   205  		return
   206  	}
   207  	if wd == "/" {
   208  		n.parent = newNavColumn(nil, nil)
   209  	} else {
   210  		all, err := n.loaddir("..")
   211  		if err != nil {
   212  			n.parent = newErrNavColumn(err)
   213  			return
   214  		}
   215  		cwd, err := os.Stat(".")
   216  		if err != nil {
   217  			n.parent = newErrNavColumn(err)
   218  			return
   219  		}
   220  		n.parent = newNavColumn(all, func(i int) bool {
   221  			d, _ := os.Lstat("../" + all[i].Text)
   222  			return os.SameFile(d, cwd)
   223  		})
   224  	}
   225  }
   226  
   227  func (n *navigation) refreshDirPreview() {
   228  	if n.current.selected != -1 {
   229  		name := n.current.selectedName()
   230  		fi, err := os.Stat(name)
   231  		if err != nil {
   232  			n.preview = newErrNavColumn(err)
   233  			return
   234  		}
   235  		if fi.Mode().IsDir() {
   236  			all, err := n.loaddir(name)
   237  			if err != nil {
   238  				n.preview = newErrNavColumn(err)
   239  				return
   240  			}
   241  			n.preview = newNavColumn(all, func(int) bool { return false })
   242  		} else {
   243  			n.preview = makeNavFilePreview(name)
   244  		}
   245  	} else {
   246  		n.preview = nil
   247  	}
   248  }
   249  
   250  // refresh rereads files in current and parent directories and maintains the
   251  // selected file if possible.
   252  func (n *navigation) refresh() {
   253  	n.refreshCurrent()
   254  	n.refreshParent()
   255  	n.refreshDirPreview()
   256  }
   257  
   258  // ascend changes current directory to the parent.
   259  // TODO(xiaq): navigation.{ascend descend} bypasses the cd builtin. This can be
   260  // problematic if cd acquires more functionality (e.g. trigger a hook).
   261  func (n *navigation) ascend() error {
   262  	wd, err := os.Getwd()
   263  	if err != nil {
   264  		return err
   265  	}
   266  	if wd == "/" {
   267  		return nil
   268  	}
   269  
   270  	name := n.parent.selectedName()
   271  	err = os.Chdir("..")
   272  	if err != nil {
   273  		return err
   274  	}
   275  	n.filter = ""
   276  	n.refresh()
   277  	n.maintainSelected(name)
   278  	// XXX Refresh dir preview again. We should perhaps not have used refresh
   279  	// above.
   280  	n.refreshDirPreview()
   281  	return nil
   282  }
   283  
   284  // descend changes current directory to the selected file, if it is a
   285  // directory.
   286  func (n *navigation) descend() error {
   287  	if n.current.selected == -1 {
   288  		return errorEmptyCwd
   289  	}
   290  	name := n.current.selectedName()
   291  	err := n.chdir(name)
   292  	if err != nil {
   293  		return err
   294  	}
   295  	n.filter = ""
   296  	n.current.selected = -1
   297  	n.refresh()
   298  	n.refreshDirPreview()
   299  	return nil
   300  }
   301  
   302  // prev selects the previous file.
   303  func (n *navigation) prev() {
   304  	if n.current.selected > 0 {
   305  		n.current.selected--
   306  	}
   307  	n.refresh()
   308  }
   309  
   310  // next selects the next file.
   311  func (n *navigation) next() {
   312  	if n.current.selected != -1 && n.current.selected < len(n.current.candidates)-1 {
   313  		n.current.selected++
   314  	}
   315  	n.refresh()
   316  }
   317  
   318  func (n *navigation) loaddir(dir string) ([]ui.Styled, error) {
   319  	f, err := os.Open(dir)
   320  	if err != nil {
   321  		return nil, err
   322  	}
   323  	names, err := f.Readdirnames(-1)
   324  	if err != nil {
   325  		return nil, err
   326  	}
   327  	sort.Strings(names)
   328  
   329  	var all []ui.Styled
   330  	lsColor := lscolors.GetColorist()
   331  	for _, name := range names {
   332  		if n.showHidden || name[0] != '.' {
   333  			all = append(all, ui.Styled{name,
   334  				ui.StylesFromString(lsColor.GetStyle(path.Join(dir, name)))})
   335  		}
   336  	}
   337  
   338  	return all, nil
   339  }
   340  
   341  func (n *navigation) List(maxHeight int) ui.Renderer {
   342  	return makeNavRenderer(
   343  		maxHeight,
   344  		n.parent.FullWidth(maxHeight),
   345  		n.current.FullWidth(maxHeight),
   346  		n.preview.FullWidth(maxHeight),
   347  		n.parent.List(maxHeight),
   348  		n.current.List(maxHeight),
   349  		n.preview.List(maxHeight),
   350  	)
   351  }
   352  
   353  // navColumn is a column in the navigation layout.
   354  type navColumn struct {
   355  	listingMode
   356  	all        []ui.Styled
   357  	candidates []ui.Styled
   358  	// selected int
   359  	err error
   360  }
   361  
   362  func newNavColumn(all []ui.Styled, sel func(int) bool) *navColumn {
   363  	nc := &navColumn{all: all, candidates: all}
   364  	nc.provider = nc
   365  	nc.selected = -1
   366  	for i := range all {
   367  		if sel(i) {
   368  			nc.selected = i
   369  		}
   370  	}
   371  	return nc
   372  }
   373  
   374  func newErrNavColumn(err error) *navColumn {
   375  	nc := &navColumn{err: err}
   376  	nc.provider = nc
   377  	return nc
   378  }
   379  
   380  func (nc *navColumn) Placeholder() string {
   381  	if nc.err != nil {
   382  		return nc.err.Error()
   383  	}
   384  	return ""
   385  }
   386  
   387  func (nc *navColumn) Len() int {
   388  	return len(nc.candidates)
   389  }
   390  
   391  func (nc *navColumn) Show(i int) (string, ui.Styled) {
   392  	cand := nc.candidates[i]
   393  	return "", ui.Styled{" " + cand.Text + " ", cand.Styles}
   394  }
   395  
   396  func (nc *navColumn) Filter(filter string) int {
   397  	nc.candidates = nc.candidates[:0]
   398  	for _, s := range nc.all {
   399  		if strings.Contains(s.Text, filter) {
   400  			nc.candidates = append(nc.candidates, s)
   401  		}
   402  	}
   403  	return 0
   404  }
   405  
   406  func (nc *navColumn) FullWidth(h int) int {
   407  	if nc == nil {
   408  		return 0
   409  	}
   410  	if nc.err != nil {
   411  		return util.Wcswidth(nc.err.Error())
   412  	}
   413  	maxw := 0
   414  	for _, s := range nc.candidates {
   415  		maxw = max(maxw, util.Wcswidth(s.Text)+2)
   416  	}
   417  	if len(nc.candidates) > h {
   418  		maxw++
   419  	}
   420  	return maxw
   421  }
   422  
   423  func (nc *navColumn) Accept(i int, ed eddefs.Editor) {
   424  	// TODO
   425  }
   426  
   427  func (nc *navColumn) ModeTitle(i int) string {
   428  	// Not used
   429  	return ""
   430  }
   431  
   432  func (nc *navColumn) selectedName() string {
   433  	if nc == nil || nc.selected == -1 || nc.selected >= len(nc.candidates) {
   434  		return ""
   435  	}
   436  	return nc.candidates[nc.selected].Text
   437  }