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) }