elvish/edit/navigation.go
2014-04-15 10:52:39 +08:00

221 lines
4.5 KiB
Go

package edit
import (
"errors"
"os"
"path"
"sort"
)
var (
errorEmptyCwd = errors.New("current directory is empty")
errorNoCwdInParent = errors.New("could not find current directory in ..")
)
type navColumn struct {
names []string
attrs []string
selected int
err error
}
func newNavColumn(names, attrs []string) *navColumn {
nc := &navColumn{names, attrs, 0, nil}
nc.resetSelected()
return nc
}
func newErrNavColumn(err error) *navColumn {
return &navColumn{err: err}
}
func (nc *navColumn) selectedName() string {
if nc == nil || nc.selected == -1 {
return ""
}
return nc.names[nc.selected]
}
func (nc *navColumn) resetSelected() {
if nc == nil {
return
}
if len(nc.names) > 0 {
nc.selected = 0
} else {
nc.selected = -1
}
}
// TODO(xiaq): Handle pwd = / correctly in navigation mode
// TODO(xiaq): Support file preview in navigation mode
type navigation struct {
current, parent, dirPreview *navColumn
}
func newNavigation() *navigation {
n := &navigation{}
n.refresh()
return n
}
func readdirnames(dir string) (names, attrs []string, err error) {
f, err := os.Open(dir)
if err != nil {
return nil, nil, err
}
names, err = f.Readdirnames(0)
if err != nil {
return nil, nil, err
}
sort.Strings(names)
attrs = make([]string, len(names))
for i, name := range names {
attrs[i] = defaultLsColor.determineAttr(path.Join(dir, name))
}
return names, attrs, nil
}
func (n *navigation) maintainSelected(name string) {
i := sort.SearchStrings(n.current.names, name)
if i == len(n.current.names) {
i--
}
n.current.selected = i
}
func (n *navigation) refreshCurrent() {
selectedName := n.current.selectedName()
names, attrs, err := readdirnames(".")
if err != nil {
n.current = newErrNavColumn(err)
return
}
n.current = newNavColumn(names, attrs)
if selectedName != "" {
// Maintain n.current.selected. The same file, if still present, is
// selected. Otherwise a file near it is selected.
// XXX(xiaq): This would break when we support alternative
// ordering.
n.maintainSelected(selectedName)
}
}
func (n *navigation) refreshParent() {
wd, err := os.Getwd()
if err != nil {
n.parent = newErrNavColumn(err)
return
}
if wd == "/" {
n.parent = newNavColumn(nil, nil)
} else {
names, attrs, err := readdirnames("..")
if err != nil {
n.parent = newErrNavColumn(err)
return
}
n.parent = newNavColumn(names, attrs)
cwd, err := os.Stat(".")
if err != nil {
n.parent = newErrNavColumn(err)
return
}
n.parent.selected = -1
for i, name := range n.parent.names {
d, _ := os.Lstat("../" + name)
if os.SameFile(d, cwd) {
n.parent.selected = i
break
}
}
}
}
func (n *navigation) refreshDirPreview() {
if n.current.selected != -1 {
name := n.current.selectedName()
fi, err := os.Stat(name)
if err != nil {
n.dirPreview = newErrNavColumn(err)
return
}
if fi.Mode().IsDir() {
names, attrs, err := readdirnames(name)
if err != nil {
n.dirPreview = newErrNavColumn(err)
return
}
n.dirPreview = newNavColumn(names, attrs)
} else {
// TODO(xiaq): Support regular file preview in navigation mode
n.dirPreview = nil
}
} else {
n.dirPreview = nil
}
}
// refresh rereads files in current and parent directories and maintains the
// selected file if possible.
func (n *navigation) refresh() {
n.refreshCurrent()
n.refreshParent()
n.refreshDirPreview()
}
// ascend changes current directory to the parent.
// TODO(xiaq): navigation.{ascend descend} bypasses the cd builtin. This can be
// problematic if cd acquires more functionality (e.g. trigger a hook).
func (n *navigation) ascend() error {
wd, err := os.Getwd()
if err != nil {
return err
}
if wd == "/" {
return nil
}
name := n.parent.names[n.parent.selected]
err = os.Chdir("..")
if err != nil {
return err
}
n.refresh()
n.maintainSelected(name)
return nil
}
// descend changes current directory to the selected file, if it is a
// directory.
func (n *navigation) descend() error {
if n.current.selected == -1 {
return errorEmptyCwd
}
name := n.current.names[n.current.selected]
err := os.Chdir(name)
if err != nil {
return err
}
n.refresh()
n.current.resetSelected()
return nil
}
// prev selects the previous file.
func (n *navigation) prev() {
if n.current.selected > 0 {
n.current.selected--
}
n.refresh()
}
// next selects the next file.
func (n *navigation) next() {
if n.current.selected != -1 && n.current.selected < len(n.current.names)-1 {
n.current.selected++
}
n.refresh()
}