github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/edit/listing.go (about)

     1  package edit
     2  
     3  import (
     4  	"os"
     5  
     6  	"github.com/markusbkk/elvish/pkg/cli"
     7  	"github.com/markusbkk/elvish/pkg/cli/histutil"
     8  	"github.com/markusbkk/elvish/pkg/cli/modes"
     9  	"github.com/markusbkk/elvish/pkg/cli/tk"
    10  	"github.com/markusbkk/elvish/pkg/edit/filter"
    11  	"github.com/markusbkk/elvish/pkg/eval"
    12  	"github.com/markusbkk/elvish/pkg/eval/vals"
    13  	"github.com/markusbkk/elvish/pkg/eval/vars"
    14  	"github.com/markusbkk/elvish/pkg/store/storedefs"
    15  )
    16  
    17  func initListings(ed *Editor, ev *eval.Evaler, st storedefs.Store, histStore histutil.Store, nb eval.NsBuilder) {
    18  	bindingVar := newBindingVar(emptyBindingsMap)
    19  	app := ed.app
    20  	nb.AddNs("listing",
    21  		eval.BuildNsNamed("edit:listing").
    22  			AddVar("binding", bindingVar).
    23  			AddGoFns(map[string]interface{}{
    24  				"accept":     func() { listingAccept(app) },
    25  				"up":         func() { listingUp(app) },
    26  				"down":       func() { listingDown(app) },
    27  				"up-cycle":   func() { listingUpCycle(app) },
    28  				"down-cycle": func() { listingDownCycle(app) },
    29  				"page-up":    func() { listingPageUp(app) },
    30  				"page-down":  func() { listingPageDown(app) },
    31  				"start-custom": func(fm *eval.Frame, opts customListingOpts, items interface{}) {
    32  					listingStartCustom(ed, fm, opts, items)
    33  				},
    34  			}))
    35  
    36  	initHistlist(ed, ev, histStore, bindingVar, nb)
    37  	initLastcmd(ed, ev, histStore, bindingVar, nb)
    38  	initLocation(ed, ev, st, bindingVar, nb)
    39  }
    40  
    41  var filterSpec = modes.FilterSpec{
    42  	Maker: func(f string) func(string) bool {
    43  		q, _ := filter.Compile(f)
    44  		if q == nil {
    45  			return func(string) bool { return true }
    46  		}
    47  		return q.Match
    48  	},
    49  	Highlighter: filter.Highlight,
    50  }
    51  
    52  func initHistlist(ed *Editor, ev *eval.Evaler, histStore histutil.Store, commonBindingVar vars.PtrVar, nb eval.NsBuilder) {
    53  	bindingVar := newBindingVar(emptyBindingsMap)
    54  	bindings := newMapBindings(ed, ev, bindingVar, commonBindingVar)
    55  	dedup := newBoolVar(true)
    56  	nb.AddNs("histlist",
    57  		eval.BuildNsNamed("edit:histlist").
    58  			AddVar("binding", bindingVar).
    59  			AddGoFns(map[string]interface{}{
    60  				"start": func() {
    61  					w, err := modes.NewHistlist(ed.app, modes.HistlistSpec{
    62  						Bindings: bindings,
    63  						AllCmds:  histStore.AllCmds,
    64  						Dedup: func() bool {
    65  							return dedup.Get().(bool)
    66  						},
    67  						Filter: filterSpec,
    68  					})
    69  					startMode(ed.app, w, err)
    70  				},
    71  				"toggle-dedup": func() {
    72  					dedup.Set(!dedup.Get().(bool))
    73  					listingRefilter(ed.app)
    74  					ed.app.Redraw()
    75  				},
    76  			}))
    77  }
    78  
    79  func initLastcmd(ed *Editor, ev *eval.Evaler, histStore histutil.Store, commonBindingVar vars.PtrVar, nb eval.NsBuilder) {
    80  	bindingVar := newBindingVar(emptyBindingsMap)
    81  	bindings := newMapBindings(ed, ev, bindingVar, commonBindingVar)
    82  	nb.AddNs("lastcmd",
    83  		eval.BuildNsNamed("edit:lastcmd").
    84  			AddVar("binding", bindingVar).
    85  			AddGoFn("start", func() {
    86  				// TODO: Specify wordifier
    87  				w, err := modes.NewLastcmd(ed.app, modes.LastcmdSpec{
    88  					Bindings: bindings, Store: histStore})
    89  				startMode(ed.app, w, err)
    90  			}))
    91  }
    92  
    93  func initLocation(ed *Editor, ev *eval.Evaler, st storedefs.Store, commonBindingVar vars.PtrVar, nb eval.NsBuilder) {
    94  	bindingVar := newBindingVar(emptyBindingsMap)
    95  	pinnedVar := newListVar(vals.EmptyList)
    96  	hiddenVar := newListVar(vals.EmptyList)
    97  	workspacesVar := newMapVar(vals.EmptyMap)
    98  
    99  	bindings := newMapBindings(ed, ev, bindingVar, commonBindingVar)
   100  	workspaceIterator := modes.LocationWSIterator(
   101  		adaptToIterateStringPair(workspacesVar))
   102  
   103  	nb.AddNs("location",
   104  		eval.BuildNsNamed("edit:location").
   105  			AddVars(map[string]vars.Var{
   106  				"binding":    bindingVar,
   107  				"hidden":     hiddenVar,
   108  				"pinned":     pinnedVar,
   109  				"workspaces": workspacesVar,
   110  			}).
   111  			AddGoFn("start", func() {
   112  				w, err := modes.NewLocation(ed.app, modes.LocationSpec{
   113  					Bindings: bindings, Store: dirStore{ev, st},
   114  					IteratePinned:     adaptToIterateString(pinnedVar),
   115  					IterateHidden:     adaptToIterateString(hiddenVar),
   116  					IterateWorkspaces: workspaceIterator,
   117  					Filter:            filterSpec,
   118  				})
   119  				startMode(ed.app, w, err)
   120  			}))
   121  	ev.AddAfterChdir(func(string) {
   122  		wd, err := os.Getwd()
   123  		if err != nil {
   124  			// TODO(xiaq): Surface the error.
   125  			return
   126  		}
   127  		if st != nil {
   128  			st.AddDir(wd, 1)
   129  			kind, root := workspaceIterator.Parse(wd)
   130  			if kind != "" {
   131  				st.AddDir(kind+wd[len(root):], 1)
   132  			}
   133  		}
   134  	})
   135  }
   136  
   137  //elvdoc:fn listing:accept
   138  //
   139  // Accepts the current selected listing item.
   140  
   141  func listingAccept(app cli.App) {
   142  	if w, ok := activeComboBox(app); ok {
   143  		w.ListBox().Accept()
   144  	}
   145  }
   146  
   147  //elvdoc:fn listing:up
   148  //
   149  // Moves the cursor up in listing mode.
   150  
   151  func listingUp(app cli.App) { listingSelect(app, tk.Prev) }
   152  
   153  //elvdoc:fn listing:down
   154  //
   155  // Moves the cursor down in listing mode.
   156  
   157  func listingDown(app cli.App) { listingSelect(app, tk.Next) }
   158  
   159  //elvdoc:fn listing:up-cycle
   160  //
   161  // Moves the cursor up in listing mode, or to the last item if the first item is
   162  // currently selected.
   163  
   164  func listingUpCycle(app cli.App) { listingSelect(app, tk.PrevWrap) }
   165  
   166  //elvdoc:fn listing:down-cycle
   167  //
   168  // Moves the cursor down in listing mode, or to the first item if the last item is
   169  // currently selected.
   170  
   171  func listingDownCycle(app cli.App) { listingSelect(app, tk.NextWrap) }
   172  
   173  //elvdoc:fn listing:page-up
   174  //
   175  // Moves the cursor up one page.
   176  
   177  func listingPageUp(app cli.App) { listingSelect(app, tk.PrevPage) }
   178  
   179  //elvdoc:fn listing:page-down
   180  //
   181  // Moves the cursor down one page.
   182  
   183  func listingPageDown(app cli.App) { listingSelect(app, tk.NextPage) }
   184  
   185  //elvdoc:fn listing:left
   186  //
   187  // Moves the cursor left in listing mode.
   188  
   189  func listingLeft(app cli.App) { listingSelect(app, tk.Left) }
   190  
   191  //elvdoc:fn listing:right
   192  //
   193  // Moves the cursor right in listing mode.
   194  
   195  func listingRight(app cli.App) { listingSelect(app, tk.Right) }
   196  
   197  func listingSelect(app cli.App, f func(tk.ListBoxState) int) {
   198  	if w, ok := activeComboBox(app); ok {
   199  		w.ListBox().Select(f)
   200  	}
   201  }
   202  
   203  func listingRefilter(app cli.App) {
   204  	if w, ok := activeComboBox(app); ok {
   205  		w.Refilter()
   206  	}
   207  }
   208  
   209  //elvdoc:var location:hidden
   210  //
   211  // A list of directories to hide in the location addon.
   212  
   213  //elvdoc:var location:pinned
   214  //
   215  // A list of directories to always show at the top of the list of the location
   216  // addon.
   217  
   218  //elvdoc:var location:workspaces
   219  //
   220  // A map mapping types of workspaces to their patterns.
   221  
   222  func adaptToIterateString(variable vars.Var) func(func(string)) {
   223  	return func(f func(s string)) {
   224  		vals.Iterate(variable.Get(), func(v interface{}) bool {
   225  			f(vals.ToString(v))
   226  			return true
   227  		})
   228  	}
   229  }
   230  
   231  func adaptToIterateStringPair(variable vars.Var) func(func(string, string) bool) {
   232  	return func(f func(a, b string) bool) {
   233  		m := variable.Get().(vals.Map)
   234  		for it := m.Iterator(); it.HasElem(); it.Next() {
   235  			k, v := it.Elem()
   236  			ks, kok := k.(string)
   237  			vs, vok := v.(string)
   238  			if kok && vok {
   239  				next := f(ks, vs)
   240  				if !next {
   241  					break
   242  				}
   243  			}
   244  		}
   245  	}
   246  }
   247  
   248  // Wraps an Evaler to implement the cli.DirStore interface.
   249  type dirStore struct {
   250  	ev *eval.Evaler
   251  	st storedefs.Store
   252  }
   253  
   254  func (d dirStore) Chdir(path string) error {
   255  	return d.ev.Chdir(path)
   256  }
   257  
   258  func (d dirStore) Dirs(blacklist map[string]struct{}) ([]storedefs.Dir, error) {
   259  	if d.st == nil {
   260  		// A "no daemon" build won't have have a storedefs.Store object.
   261  		// Fail gracefully rather than panic.
   262  		return []storedefs.Dir{}, nil
   263  	}
   264  	return d.st.Dirs(blacklist)
   265  }
   266  
   267  func (d dirStore) Getwd() (string, error) {
   268  	return os.Getwd()
   269  }
   270  
   271  func startMode(app cli.App, w tk.Widget, err error) {
   272  	if w != nil {
   273  		app.PushAddon(w)
   274  		app.Redraw()
   275  	}
   276  	if err != nil {
   277  		app.Notify(modes.ErrorText(err))
   278  	}
   279  }
   280  
   281  func activeComboBox(app cli.App) (tk.ComboBox, bool) {
   282  	w, ok := app.ActiveWidget().(tk.ComboBox)
   283  	return w, ok
   284  }