github.com/kolbycrouch/elvish@v0.14.1-0.20210614162631-215b9ac1c423/pkg/cli/mode/lastcmd.go (about) 1 package mode 2 3 import ( 4 "fmt" 5 "strconv" 6 "strings" 7 8 "src.elv.sh/pkg/cli" 9 "src.elv.sh/pkg/cli/histutil" 10 "src.elv.sh/pkg/cli/tk" 11 "src.elv.sh/pkg/ui" 12 ) 13 14 // Lastcmd is a mode for inspecting the last command, and inserting part of all 15 // of it. It is based on the ComboBox widget. 16 type Lastcmd interface { 17 tk.ComboBox 18 } 19 20 // LastcmdSpec specifies the configuration for the lastcmd mode. 21 type LastcmdSpec struct { 22 // Key bindings. 23 Bindings tk.Bindings 24 // Store provides the source for the last command. 25 Store LastcmdStore 26 // Wordifier breaks a command into words. 27 Wordifier func(string) []string 28 } 29 30 // LastcmdStore is a subset of histutil.Store used in lastcmd mode. 31 type LastcmdStore interface { 32 Cursor(prefix string) histutil.Cursor 33 } 34 35 var _ = LastcmdStore(histutil.Store(nil)) 36 37 // NewLastcmd creates a new lastcmd mode. 38 func NewLastcmd(app cli.App, cfg LastcmdSpec) (Lastcmd, error) { 39 if cfg.Store == nil { 40 return nil, errNoHistoryStore 41 } 42 c := cfg.Store.Cursor("") 43 c.Prev() 44 cmd, err := c.Get() 45 if err != nil { 46 return nil, fmt.Errorf("db error: %v", err) 47 } 48 wordifier := cfg.Wordifier 49 if wordifier == nil { 50 wordifier = strings.Fields 51 } 52 cmdText := cmd.Text 53 words := wordifier(cmdText) 54 entries := make([]lastcmdEntry, len(words)+1) 55 entries[0] = lastcmdEntry{content: cmdText} 56 for i, word := range words { 57 entries[i+1] = lastcmdEntry{strconv.Itoa(i), strconv.Itoa(i - len(words)), word} 58 } 59 60 accept := func(text string) { 61 app.CodeArea().MutateState(func(s *tk.CodeAreaState) { 62 s.Buffer.InsertAtDot(text) 63 }) 64 app.SetAddon(nil, false) 65 } 66 w := tk.NewComboBox(tk.ComboBoxSpec{ 67 CodeArea: tk.CodeAreaSpec{Prompt: modePrompt(" LASTCMD ", true)}, 68 ListBox: tk.ListBoxSpec{ 69 Bindings: cfg.Bindings, 70 OnAccept: func(it tk.Items, i int) { 71 accept(it.(lastcmdItems).entries[i].content) 72 }, 73 }, 74 OnFilter: func(w tk.ComboBox, p string) { 75 items := filterLastcmdItems(entries, p) 76 if len(items.entries) == 1 { 77 accept(items.entries[0].content) 78 } else { 79 w.ListBox().Reset(items, 0) 80 } 81 }, 82 }) 83 return w, nil 84 } 85 86 type lastcmdItems struct { 87 negFilter bool 88 entries []lastcmdEntry 89 } 90 91 type lastcmdEntry struct { 92 posIndex string 93 negIndex string 94 content string 95 } 96 97 func filterLastcmdItems(allEntries []lastcmdEntry, p string) lastcmdItems { 98 if p == "" { 99 return lastcmdItems{false, allEntries} 100 } 101 var entries []lastcmdEntry 102 negFilter := strings.HasPrefix(p, "-") 103 for _, entry := range allEntries { 104 if (negFilter && strings.HasPrefix(entry.negIndex, p)) || 105 (!negFilter && strings.HasPrefix(entry.posIndex, p)) { 106 entries = append(entries, entry) 107 } 108 } 109 return lastcmdItems{negFilter, entries} 110 } 111 112 func (it lastcmdItems) Show(i int) ui.Text { 113 index := "" 114 entry := it.entries[i] 115 if it.negFilter { 116 index = entry.negIndex 117 } else { 118 index = entry.posIndex 119 } 120 // NOTE: We now use a hardcoded width of 3 for the index, which will work as 121 // long as the command has less than 1000 words (when filter is positive) or 122 // 100 words (when filter is negative). 123 return ui.T(fmt.Sprintf("%3s %s", index, entry.content)) 124 } 125 126 func (it lastcmdItems) Len() int { return len(it.entries) }