listing.go 6.2 KB

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