github.com/elves/elvish@v0.15.0/pkg/cli/addons/lastcmd/lastcmd.go (about) 1 // Package lastcmd implements an addon that supports inserting the last command 2 // or words from it. 3 package lastcmd 4 5 import ( 6 "fmt" 7 "strconv" 8 "strings" 9 10 "github.com/elves/elvish/pkg/cli" 11 "github.com/elves/elvish/pkg/cli/histutil" 12 "github.com/elves/elvish/pkg/ui" 13 ) 14 15 // Config is the configuration for starting lastcmd. 16 type Config struct { 17 // Binding provides key binding. 18 Binding cli.Handler 19 // Store provides the source for the last command. 20 Store Store 21 // Wordifier breaks a command into words. 22 Wordifier func(string) []string 23 } 24 25 // Store wraps the LastCmd method. It is a subset of histutil.Store. 26 type Store interface { 27 Cursor(prefix string) histutil.Cursor 28 } 29 30 var _ = Store(histutil.Store(nil)) 31 32 // Start starts lastcmd function. 33 func Start(app cli.App, cfg Config) { 34 if cfg.Store == nil { 35 app.Notify("no history store") 36 return 37 } 38 c := cfg.Store.Cursor("") 39 c.Prev() 40 cmd, err := c.Get() 41 if err != nil { 42 app.Notify("db error: " + err.Error()) 43 return 44 } 45 wordifier := cfg.Wordifier 46 if wordifier == nil { 47 wordifier = strings.Fields 48 } 49 cmdText := cmd.Text 50 words := wordifier(cmdText) 51 entries := make([]entry, len(words)+1) 52 entries[0] = entry{content: cmdText} 53 for i, word := range words { 54 entries[i+1] = entry{strconv.Itoa(i), strconv.Itoa(i - len(words)), word} 55 } 56 57 accept := func(text string) { 58 app.CodeArea().MutateState(func(s *cli.CodeAreaState) { 59 s.Buffer.InsertAtDot(text) 60 }) 61 app.MutateState(func(s *cli.State) { s.Addon = nil }) 62 } 63 w := cli.NewComboBox(cli.ComboBoxSpec{ 64 CodeArea: cli.CodeAreaSpec{Prompt: cli.ModePrompt(" LASTCMD ", true)}, 65 ListBox: cli.ListBoxSpec{ 66 OverlayHandler: cfg.Binding, 67 OnAccept: func(it cli.Items, i int) { 68 accept(it.(items).entries[i].content) 69 }, 70 }, 71 OnFilter: func(w cli.ComboBox, p string) { 72 items := filter(entries, p) 73 if len(items.entries) == 1 { 74 accept(items.entries[0].content) 75 } else { 76 w.ListBox().Reset(items, 0) 77 } 78 }, 79 }) 80 app.MutateState(func(s *cli.State) { s.Addon = w }) 81 app.Redraw() 82 } 83 84 type items struct { 85 negFilter bool 86 entries []entry 87 } 88 89 type entry struct { 90 posIndex string 91 negIndex string 92 content string 93 } 94 95 func filter(allEntries []entry, p string) items { 96 if p == "" { 97 return items{false, allEntries} 98 } 99 var entries []entry 100 negFilter := strings.HasPrefix(p, "-") 101 for _, entry := range allEntries { 102 if (negFilter && strings.HasPrefix(entry.negIndex, p)) || 103 (!negFilter && strings.HasPrefix(entry.posIndex, p)) { 104 entries = append(entries, entry) 105 } 106 } 107 return items{negFilter, entries} 108 } 109 110 func (it items) Show(i int) ui.Text { 111 index := "" 112 entry := it.entries[i] 113 if it.negFilter { 114 index = entry.negIndex 115 } else { 116 index = entry.posIndex 117 } 118 // NOTE: We now use a hardcoded width of 3 for the index, which will work as 119 // long as the command has less than 1000 words (when filter is positive) or 120 // 100 words (when filter is negative). 121 return ui.T(fmt.Sprintf("%3s %s", index, entry.content)) 122 } 123 124 func (it items) Len() int { return len(it.entries) }