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 }