src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/edit/listing.go (about)

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