diff --git a/edit/bind.go b/edit/bind.go index ad7310e5..85c53835 100644 --- a/edit/bind.go +++ b/edit/bind.go @@ -40,6 +40,7 @@ var defaultBindings = map[bufferMode]map[Key]string{ Key{Up, 0}: "start-history", Key{'N', Ctrl}: "start-navigation", Key{'H', Ctrl}: "start-history-listing", + Key{'L', Ctrl}: "start-location", }, modeCommand: map[Key]string{ Default: "default-command", @@ -85,6 +86,14 @@ var defaultBindings = map[bufferMode]map[Key]string{ modeHistoryListing: map[Key]string{ Default: "default-history-listing", }, + modeLocation: map[Key]string{ + Key{Up, 0}: "location-prev", + Key{Down, 0}: "location-next", + Key{Tab, 0}: "location-next", + Key{Enter, 0}: "accept-location", + Key{'[', Ctrl}: "cancel-location", + Default: "location-default", + }, } var keyBindings = map[bufferMode]map[Key]Caller{} diff --git a/edit/builtins.go b/edit/builtins.go index c6772e53..86546d00 100644 --- a/edit/builtins.go +++ b/edit/builtins.go @@ -74,6 +74,14 @@ var builtins = []Builtin{ {"start-history-listing", startHistoryListing}, {"default-history-listing", defaultHistoryListing}, + // Location mode + {"start-location", startLocation}, + {"location-prev", locationPrev}, + {"location-next", locationNext}, + {"accept-location", acceptLocation}, + {"cancel-location", cancelLocation}, + {"location-default", locationDefault}, + // Misc {"redraw", redraw}, } diff --git a/edit/editor.go b/edit/editor.go index 1ca9d0fb..93ab5e24 100644 --- a/edit/editor.go +++ b/edit/editor.go @@ -49,6 +49,7 @@ type editorState struct { navigation *navigation history historyState historyListing *historyListing + location location isExternal map[string]bool parseErrorAtEnd bool // Used for builtins. @@ -65,6 +66,7 @@ const ( modeNavigation modeHistory modeHistoryListing + modeLocation ) type actionType int diff --git a/edit/location.go b/edit/location.go new file mode 100644 index 00000000..70b52633 --- /dev/null +++ b/edit/location.go @@ -0,0 +1,74 @@ +package edit + +import ( + "os" + "unicode" + + "github.com/elves/elvish/store" +) + +type location struct { + filter string + candidates []store.Dir + current int +} + +func (l *location) updateCandidates(store *store.Store) error { + dirs, err := store.FindDirs(l.filter) + if err != nil { + return err + } + l.candidates = dirs + + if len(l.candidates) > 0 { + l.current = 0 + } else { + l.current = -1 + } + return nil +} + +func startLocation(ed *Editor) { + ed.location = location{} + ed.location.updateCandidates(ed.store) + ed.mode = modeLocation +} + +func locationPrev(ed *Editor) { + if len(ed.location.candidates) > 0 && ed.location.current > 0 { + ed.location.current-- + } +} + +func locationNext(ed *Editor) { + if len(ed.location.candidates) > 0 && ed.location.current < len(ed.location.candidates)-1 { + ed.location.current++ + } +} + +func acceptLocation(ed *Editor) { + // XXX Maybe we want to use eval.cdInner and increase the score? + loc := &ed.location + if len(loc.candidates) > 0 { + err := os.Chdir(loc.candidates[loc.current].Path) + if err != nil { + ed.notify("%v", err) + } + } + ed.mode = modeInsert +} + +func cancelLocation(ed *Editor) { + ed.mode = modeInsert +} + +func locationDefault(ed *Editor) { + k := ed.lastKey + if k.Mod == 0 && k.Rune > 0 && unicode.IsGraphic(k.Rune) { + ed.location.filter += string(k.Rune) + ed.location.updateCandidates(ed.store) + } else { + cancelLocation(ed) + ed.nextAction = action{actionType: reprocessKey} + } +} diff --git a/edit/style.go b/edit/style.go index fca53149..987a85f7 100644 --- a/edit/style.go +++ b/edit/style.go @@ -10,6 +10,8 @@ var ( styleForCurrentCompletion = ";7" styleForCompletedHistory = "2" styleForSelectedFile = ";7" + styleForLocation = "4" + styleForSelectedLocation = ";7" ) var styleForType = map[TokenKind]string{ diff --git a/edit/writer.go b/edit/writer.go index d8ca2b9e..3beb2c83 100644 --- a/edit/writer.go +++ b/edit/writer.go @@ -8,6 +8,7 @@ import ( "unicode" "unicode/utf8" + "github.com/elves/elvish/parse" "github.com/elves/elvish/sys" ) @@ -483,9 +484,15 @@ tokens: case modeHistory: text = fmt.Sprintf("HISTORY #%d", es.history.current) case modeHistoryListing: - text = fmt.Sprintf("HISTORY LISTING") + text = "HISTORY LISTING" + case modeLocation: + text = "LOCATION" } b.writes(TrimWcWidth(" "+text+" ", width), styleForMode) + if es.mode == modeLocation { + b.writes(" ", "") + b.writes(es.location.filter, styleForLocation) + } } // bufTips @@ -528,10 +535,11 @@ tokens: // Render bufListing under the maximum height constraint nav := es.navigation hist := es.historyListing - if hListing > 0 && (comp != nil || nav != nil || hist != nil) { + if hListing > 0 { b := newBuffer(width) bufListing = b - if comp != nil { // Completion listing + switch es.mode { + case modeCompletion: // Layout candidates in multiple columns cands := comp.candidates @@ -574,7 +582,7 @@ tokens: b.writes(ForceWcWidth(text, colWidth), style) } } - } else if nav != nil { // Navigation listing + case modeNavigation: margin := navigationListingColMargin var ratioParent, ratioCurrent, ratioPreview int if nav.dirPreview != nil { @@ -603,7 +611,7 @@ tokens: bPreview := renderNavColumn(nav.dirPreview, wPreview, hListing) b.extendHorizontal(bPreview, wParent+wCurrent+margin, margin) } - } else if hist != nil { + case modeHistoryListing: n := len(hist.all) i := 0 @@ -625,6 +633,26 @@ tokens: if len(b.cells) > 0 { b.trimToLines(startIndex, n) } + case modeLocation: + loc := &es.location + if len(loc.candidates) == 0 { + b.writes("(no match)", "") + break + } + low, high := findWindow(len(loc.candidates), loc.current, hListing) + for i := low; i < high; i++ { + if i > low { + b.newline() + } + text := fmt.Sprintf("%4.0f %s", loc.candidates[i].Score, parse.Quote(loc.candidates[i].Path)) + style := "" + if i == loc.current { + style = styleForSelectedLocation + } + b.writes(TrimWcWidth(text, width), style) + } + default: + bufListing = nil } }