github.com/hoop33/elvish@v0.0.0-20160801152013-6d25485beab4/edit/navigation.go (about)

     1  package edit
     2  
     3  import (
     4  	"errors"
     5  	"os"
     6  	"path"
     7  	"strings"
     8  	"unicode/utf8"
     9  
    10  	"github.com/elves/elvish/parse"
    11  )
    12  
    13  // Navigation subsystem.
    14  
    15  // Interface.
    16  
    17  type navigation struct {
    18  	current    *navColumn
    19  	parent     *navColumn
    20  	dirPreview *navColumn
    21  	showHidden bool
    22  	filtering  bool
    23  	filter     string
    24  }
    25  
    26  func (*navigation) Mode() ModeType {
    27  	return modeNavigation
    28  }
    29  
    30  func (n *navigation) ModeLine(width int) *buffer {
    31  	s := " NAVIGATING "
    32  	if n.showHidden {
    33  		s += "(show hidden) "
    34  	}
    35  	b := newBuffer(width)
    36  	b.writes(TrimWcWidth(s, width), styleForMode)
    37  	b.writes(" ", "")
    38  	b.writes(n.filter, styleForFilter)
    39  	b.dot = b.cursor()
    40  	return b
    41  }
    42  
    43  func startNav(ed *Editor) {
    44  	initNavigation(&ed.navigation)
    45  	ed.mode = &ed.navigation
    46  }
    47  
    48  func navUp(ed *Editor) {
    49  	ed.navigation.prev()
    50  }
    51  
    52  func navDown(ed *Editor) {
    53  	ed.navigation.next()
    54  }
    55  
    56  func navPageUp(ed *Editor) {
    57  	ed.navigation.current.pageUp()
    58  }
    59  
    60  func navPageDown(ed *Editor) {
    61  	ed.navigation.current.pageDown()
    62  }
    63  
    64  func navLeft(ed *Editor) {
    65  	ed.navigation.ascend()
    66  }
    67  
    68  func navRight(ed *Editor) {
    69  	ed.navigation.descend()
    70  }
    71  
    72  func navTriggerShowHidden(ed *Editor) {
    73  	ed.navigation.showHidden = !ed.navigation.showHidden
    74  	ed.navigation.refresh()
    75  }
    76  
    77  func navTriggerFilter(ed *Editor) {
    78  	ed.navigation.filtering = !ed.navigation.filtering
    79  }
    80  
    81  func navInsertSelected(ed *Editor) {
    82  	ed.insertAtDot(parse.Quote(ed.navigation.current.selectedName()) + " ")
    83  }
    84  
    85  func navigationDefault(ed *Editor) {
    86  	// Use key binding for insert mode without exiting nigation mode.
    87  	k := ed.lastKey
    88  	n := &ed.navigation
    89  	if n.filtering && likeChar(k) {
    90  		n.filter += k.String()
    91  		n.refreshCurrent()
    92  	} else if n.filtering && k == (Key{Backspace, 0}) {
    93  		_, size := utf8.DecodeLastRuneInString(n.filter)
    94  		if size > 0 {
    95  			n.filter = n.filter[:len(n.filter)-size]
    96  			n.refreshCurrent()
    97  		}
    98  	} else if f, ok := keyBindings[modeInsert][k]; ok {
    99  		f.Call(ed)
   100  	} else {
   101  		keyBindings[modeInsert][Default].Call(ed)
   102  	}
   103  }
   104  
   105  // Implementation.
   106  // TODO(xiaq): Support file preview in navigation mode
   107  // TODO(xiaq): Remember which file was selected in each directory.
   108  
   109  var (
   110  	errorEmptyCwd      = errors.New("current directory is empty")
   111  	errorNoCwdInParent = errors.New("could not find current directory in ..")
   112  )
   113  
   114  func initNavigation(n *navigation) {
   115  	*n = navigation{}
   116  	n.refresh()
   117  }
   118  
   119  func (n *navigation) maintainSelected(name string) {
   120  	n.current.selected = 0
   121  	for i, s := range n.current.candidates {
   122  		if s.text > name {
   123  			break
   124  		}
   125  		n.current.selected = i
   126  	}
   127  }
   128  
   129  func (n *navigation) refreshCurrent() {
   130  	selectedName := n.current.selectedName()
   131  	all, err := n.loaddir(".")
   132  	if err != nil {
   133  		n.current = newErrNavColumn(err)
   134  		return
   135  	}
   136  	// Try to select the old selected file.
   137  	// XXX(xiaq): This would break when we support alternative ordering.
   138  	n.current = newNavColumn(all, func(i int) bool {
   139  		return i == 0 || all[i].text <= selectedName
   140  	})
   141  	n.current.changeFilter(n.filter)
   142  	n.maintainSelected(selectedName)
   143  }
   144  
   145  func (n *navigation) refreshParent() {
   146  	wd, err := os.Getwd()
   147  	if err != nil {
   148  		n.parent = newErrNavColumn(err)
   149  		return
   150  	}
   151  	if wd == "/" {
   152  		n.parent = newNavColumn(nil, nil)
   153  	} else {
   154  		all, err := n.loaddir("..")
   155  		if err != nil {
   156  			n.parent = newErrNavColumn(err)
   157  			return
   158  		}
   159  		cwd, err := os.Stat(".")
   160  		if err != nil {
   161  			n.parent = newErrNavColumn(err)
   162  			return
   163  		}
   164  		n.parent = newNavColumn(all, func(i int) bool {
   165  			d, _ := os.Lstat("../" + all[i].text)
   166  			return os.SameFile(d, cwd)
   167  		})
   168  	}
   169  }
   170  
   171  func (n *navigation) refreshDirPreview() {
   172  	if n.current.selected != -1 {
   173  		name := n.current.selectedName()
   174  		fi, err := os.Stat(name)
   175  		if err != nil {
   176  			n.dirPreview = newErrNavColumn(err)
   177  			return
   178  		}
   179  		if fi.Mode().IsDir() {
   180  			all, err := n.loaddir(name)
   181  			if err != nil {
   182  				n.dirPreview = newErrNavColumn(err)
   183  				return
   184  			}
   185  			n.dirPreview = newNavColumn(all, func(int) bool { return false })
   186  		} else {
   187  			// TODO(xiaq): Support regular file preview in navigation mode
   188  			n.dirPreview = nil
   189  		}
   190  	} else {
   191  		n.dirPreview = nil
   192  	}
   193  }
   194  
   195  // refresh rereads files in current and parent directories and maintains the
   196  // selected file if possible.
   197  func (n *navigation) refresh() {
   198  	n.refreshCurrent()
   199  	n.refreshParent()
   200  	n.refreshDirPreview()
   201  }
   202  
   203  // ascend changes current directory to the parent.
   204  // TODO(xiaq): navigation.{ascend descend} bypasses the cd builtin. This can be
   205  // problematic if cd acquires more functionality (e.g. trigger a hook).
   206  func (n *navigation) ascend() error {
   207  	wd, err := os.Getwd()
   208  	if err != nil {
   209  		return err
   210  	}
   211  	if wd == "/" {
   212  		return nil
   213  	}
   214  
   215  	name := n.parent.selectedName()
   216  	err = os.Chdir("..")
   217  	if err != nil {
   218  		return err
   219  	}
   220  	n.filter = ""
   221  	n.refresh()
   222  	n.maintainSelected(name)
   223  	// XXX Refresh dir preview again. We should perhaps not have used refresh
   224  	// above.
   225  	n.refreshDirPreview()
   226  	return nil
   227  }
   228  
   229  // descend changes current directory to the selected file, if it is a
   230  // directory.
   231  func (n *navigation) descend() error {
   232  	if n.current.selected == -1 {
   233  		return errorEmptyCwd
   234  	}
   235  	name := n.current.selectedName()
   236  	err := os.Chdir(name)
   237  	if err != nil {
   238  		return err
   239  	}
   240  	n.filter = ""
   241  	n.current.selected = -1
   242  	n.refresh()
   243  	n.refreshDirPreview()
   244  	return nil
   245  }
   246  
   247  // prev selects the previous file.
   248  func (n *navigation) prev() {
   249  	if n.current.selected > 0 {
   250  		n.current.selected--
   251  	}
   252  	n.refresh()
   253  }
   254  
   255  // next selects the next file.
   256  func (n *navigation) next() {
   257  	if n.current.selected != -1 && n.current.selected < len(n.current.candidates)-1 {
   258  		n.current.selected++
   259  	}
   260  	n.refresh()
   261  }
   262  
   263  func (n *navigation) loaddir(dir string) ([]styled, error) {
   264  	f, err := os.Open(dir)
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  	infos, err := f.Readdir(0)
   269  	if err != nil {
   270  		return nil, err
   271  	}
   272  	var all []styled
   273  	for _, info := range infos {
   274  		if n.showHidden || info.Name()[0] != '.' {
   275  			name := info.Name()
   276  			all = append(all, styled{name, defaultLsColor.getStyle(path.Join(dir, name))})
   277  		}
   278  	}
   279  	sortStyleds(all)
   280  
   281  	return all, nil
   282  }
   283  
   284  const (
   285  	navigationListingColMargin          = 1
   286  	navigationListingMinWidthForPadding = 5
   287  )
   288  
   289  func (nav *navigation) List(width, maxHeight int) *buffer {
   290  	margin := navigationListingColMargin
   291  	var ratioParent, ratioCurrent, ratioPreview int
   292  	if nav.dirPreview != nil {
   293  		ratioParent = 15
   294  		ratioCurrent = 40
   295  		ratioPreview = 45
   296  	} else {
   297  		ratioParent = 15
   298  		ratioCurrent = 75
   299  		// Leave some space at the right side
   300  	}
   301  
   302  	w := width - margin*2
   303  
   304  	wParent := w * ratioParent / 100
   305  	wCurrent := w * ratioCurrent / 100
   306  	wPreview := w * ratioPreview / 100
   307  
   308  	b := renderNavColumn(nav.parent, wParent, maxHeight)
   309  
   310  	bCurrent := renderNavColumn(nav.current, wCurrent, maxHeight)
   311  	b.extendHorizontal(bCurrent, wParent+margin)
   312  
   313  	if wPreview > 0 {
   314  		bPreview := renderNavColumn(nav.dirPreview, wPreview, maxHeight)
   315  		b.extendHorizontal(bPreview, wParent+wCurrent+2*margin)
   316  	}
   317  
   318  	return b
   319  }
   320  
   321  // navColumn is a column in the navigation layout.
   322  type navColumn struct {
   323  	listing
   324  	all        []styled
   325  	candidates []styled
   326  	// selected int
   327  	err error
   328  }
   329  
   330  func newNavColumn(all []styled, sel func(int) bool) *navColumn {
   331  	nc := &navColumn{all: all, candidates: all}
   332  	nc.provider = nc
   333  	nc.selected = -1
   334  	for i := range all {
   335  		if sel(i) {
   336  			nc.selected = i
   337  		}
   338  	}
   339  	return nc
   340  }
   341  
   342  func newErrNavColumn(err error) *navColumn {
   343  	nc := &navColumn{err: err}
   344  	nc.provider = nc
   345  	return nc
   346  }
   347  
   348  func (nc *navColumn) Placeholder() string {
   349  	if nc.err != nil {
   350  		return nc.err.Error()
   351  	}
   352  	return ""
   353  }
   354  
   355  func (nc *navColumn) Len() int {
   356  	return len(nc.candidates)
   357  }
   358  
   359  func (nc *navColumn) Show(i, w int) styled {
   360  	s := nc.candidates[i]
   361  	if w >= navigationListingMinWidthForPadding {
   362  		return styled{" " + ForceWcWidth(s.text, w-2), s.style}
   363  	}
   364  	return styled{ForceWcWidth(s.text, w), s.style}
   365  }
   366  
   367  func (nc *navColumn) Filter(filter string) int {
   368  	nc.candidates = nc.candidates[:0]
   369  	for _, s := range nc.all {
   370  		if strings.Contains(s.text, filter) {
   371  			nc.candidates = append(nc.candidates, s)
   372  		}
   373  	}
   374  	return 0
   375  }
   376  
   377  func (nc *navColumn) Accept(i int, ed *Editor) {
   378  	// TODO
   379  }
   380  
   381  func (nc *navColumn) ModeTitle(i int) string {
   382  	// Not used
   383  	return ""
   384  }
   385  
   386  func (nc *navColumn) selectedName() string {
   387  	if nc == nil || nc.selected == -1 || nc.selected >= len(nc.candidates) {
   388  		return ""
   389  	}
   390  	return nc.candidates[nc.selected].text
   391  }
   392  
   393  func renderNavColumn(nc *navColumn, w, h int) *buffer {
   394  	return nc.List(w, h)
   395  }