elvish/edit/navigation.go
2016-03-23 17:33:00 +01:00

334 lines
6.9 KiB
Go

package edit
import (
"errors"
"os"
"path"
"github.com/elves/elvish/parse"
)
// Navigation subsystem.
// Interface.
type navigation struct {
current *navColumn
parent *navColumn
dirPreview *navColumn
showHidden bool
}
func (*navigation) Mode() ModeType {
return modeNavigation
}
func (*navigation) ModeLine(width int) *buffer {
return makeModeLine(" NAVIGATING ", width)
}
func startNav(ed *Editor) {
initNavigation(&ed.navigation)
ed.mode = &ed.navigation
}
func navUp(ed *Editor) {
ed.navigation.prev()
}
func navDown(ed *Editor) {
ed.navigation.next()
}
func navLeft(ed *Editor) {
ed.navigation.ascend()
}
func navRight(ed *Editor) {
ed.navigation.descend()
}
func navTriggerShowHidden(ed *Editor) {
ed.navigation.showHidden = !ed.navigation.showHidden
ed.navigation.refresh()
}
func navInsertSelected(ed *Editor) {
ed.insertAtDot(parse.Quote(ed.navigation.current.selectedName()) + " ")
}
func navigationDefault(ed *Editor) {
// Use key binding for insert mode without exiting navigation mode.
if f, ok := keyBindings[modeInsert][ed.lastKey]; ok {
f.Call(ed)
} else {
keyBindings[modeInsert][Default].Call(ed)
}
}
// Implementation.
// TODO(xiaq): Support file preview in navigation mode
// TODO(xiaq): Remember which file was selected in each directory.
var (
errorEmptyCwd = errors.New("current directory is empty")
errorNoCwdInParent = errors.New("could not find current directory in ..")
)
func initNavigation(n *navigation) {
*n = navigation{}
n.refresh()
}
func (n *navigation) maintainSelected(name string) {
n.current.selected = -1
for i, s := range n.current.all {
if s.text > name {
break
}
n.current.selected = i
}
}
func (n *navigation) refreshCurrent() {
selectedName := n.current.selectedName()
all, err := n.loaddir(".")
if err != nil {
n.current = newErrNavColumn(err)
return
}
// Try to select the old selected file.
// XXX(xiaq): This would break when we support alternative ordering.
n.current = newNavColumn(all, func(i int) bool {
return i == 0 || all[i].text <= 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 {
all, err := n.loaddir("..")
if err != nil {
n.parent = newErrNavColumn(err)
return
}
cwd, err := os.Stat(".")
if err != nil {
n.parent = newErrNavColumn(err)
return
}
n.parent = newNavColumn(all, func(i int) bool {
d, _ := os.Lstat("../" + all[i].text)
return os.SameFile(d, cwd)
})
}
}
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() {
all, err := n.loaddir(name)
if err != nil {
n.dirPreview = newErrNavColumn(err)
return
}
n.dirPreview = newNavColumn(all, func(int) bool { return false })
} 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.selectedName()
err = os.Chdir("..")
if err != nil {
return err
}
n.refresh()
n.maintainSelected(name)
// XXX Refresh dir preview again. We should perhaps not have used refresh
// above.
n.refreshDirPreview()
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.selectedName()
err := os.Chdir(name)
if err != nil {
return err
}
n.current.selected = -1
n.refresh()
n.refreshDirPreview()
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.all)-1 {
n.current.selected++
}
n.refresh()
}
// navColumn is a column in the navigation layout.
type navColumn struct {
all []styled
selected int
err error
}
func newNavColumn(all []styled, sel func(int) bool) *navColumn {
nc := &navColumn{all, 0, nil}
nc.selected = -1
for i := range all {
if sel(i) {
nc.selected = i
}
}
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.all[nc.selected].text
}
func (n *navigation) loaddir(dir string) ([]styled, error) {
f, err := os.Open(dir)
if err != nil {
return nil, err
}
infos, err := f.Readdir(0)
if err != nil {
return nil, err
}
var all []styled
for _, info := range infos {
if n.showHidden || info.Name()[0] != '.' {
name := info.Name()
all = append(all, styled{name, defaultLsColor.getStyle(path.Join(dir, name))})
}
}
sortStyleds(all)
return all, nil
}
const (
navigationListingColMargin = 1
navigationListingColPadding = 1
navigationListingMinWidthForPadding = 5
)
func (nav *navigation) List(width, maxHeight int) *buffer {
margin := navigationListingColMargin
var ratioParent, ratioCurrent, ratioPreview int
if nav.dirPreview != nil {
ratioParent = 15
ratioCurrent = 40
ratioPreview = 45
} else {
ratioParent = 15
ratioCurrent = 75
// Leave some space at the right side
}
w := width - margin*2
wParent := w * ratioParent / 100
wCurrent := w * ratioCurrent / 100
wPreview := w * ratioPreview / 100
b := renderNavColumn(nav.parent, wParent, maxHeight)
bCurrent := renderNavColumn(nav.current, wCurrent, maxHeight)
b.extendHorizontal(bCurrent, wParent, margin)
if wPreview > 0 {
bPreview := renderNavColumn(nav.dirPreview, wPreview, maxHeight)
b.extendHorizontal(bPreview, wParent+wCurrent+margin, margin)
}
return b
}
func renderNavColumn(nc *navColumn, w, h int) *buffer {
b := newBuffer(w)
low, high := findWindow(len(nc.all), nc.selected, h)
for i := low; i < high; i++ {
if i > low {
b.newline()
}
text := nc.all[i].text
style := nc.all[i].style
if i == nc.selected {
style += styleForSelected
}
if w >= navigationListingMinWidthForPadding {
padding := navigationListingColPadding
b.writePadding(padding, style)
b.writes(ForceWcWidth(text, w-2), style)
b.writePadding(padding, style)
} else {
b.writes(ForceWcWidth(text, w), style)
}
}
return b
}