gitlab.com/apertussolutions/u-root@v7.0.0+incompatible/cmds/core/elvish/edit/navigation.go (about) 1 package edit 2 3 import ( 4 "errors" 5 "os" 6 "path" 7 "sort" 8 "strings" 9 "unicode/utf8" 10 11 "github.com/u-root/u-root/cmds/core/elvish/edit/eddefs" 12 "github.com/u-root/u-root/cmds/core/elvish/edit/ui" 13 "github.com/u-root/u-root/cmds/core/elvish/eval" 14 "github.com/u-root/u-root/cmds/core/elvish/eval/vars" 15 "github.com/u-root/u-root/cmds/core/elvish/parse" 16 "github.com/u-root/u-root/cmds/core/elvish/util" 17 ) 18 19 // Navigation subsystem. 20 21 // Interface. 22 23 type navigation struct { 24 binding eddefs.BindingMap 25 chdir func(string) error 26 navigationState 27 } 28 29 type navigationState struct { 30 current *navColumn 31 parent *navColumn 32 preview navPreview 33 showHidden bool 34 filtering bool 35 filter string 36 } 37 38 func init() { atEditorInit(initNavigation) } 39 40 func initNavigation(ed *editor, ns eval.Ns) { 41 n := &navigation{ 42 binding: emptyBindingMap, 43 chdir: ed.Evaler().Chdir, 44 } 45 ed.navigation = n 46 47 subns := eval.Ns{ 48 "binding": vars.FromPtr(&n.binding), 49 } 50 subns.AddBuiltinFns("edit:navigation:", map[string]interface{}{ 51 "start": func() { n.start(ed) }, 52 "up": n.prev, 53 "down": n.next, 54 "page-up": n.pageUp, 55 "page-down": n.pageDown, 56 "left": n.ascend, 57 "right": n.descend, 58 "file-preview-up": n.filePreviewUp, 59 "file-preview-down": n.filePreviewDown, 60 "trigger-shown-hidden": n.triggerShowHidden, 61 "trigger-filter": n.triggerFilter, 62 "insert-selected": func() { n.insertSelected(ed) }, 63 "insert-selected-and-quit": func() { n.insertSelectedAndQuit(ed) }, 64 "default": func() { n.defaultFn(ed) }, 65 }) 66 ns.AddNs("navigation", subns) 67 } 68 69 type navPreview interface { 70 FullWidth(int) int 71 List(int) ui.Renderer 72 } 73 74 func (n *navigation) Teardown() { 75 n.navigationState = navigationState{} 76 } 77 78 func (n *navigation) Binding(k ui.Key) eval.Callable { 79 return n.binding.GetOrDefault(k) 80 } 81 82 func (n *navigation) ModeLine() ui.Renderer { 83 title := " NAVIGATING " 84 if n.showHidden { 85 title += "(show hidden) " 86 } 87 return ui.NewModeLineRenderer(title, n.filter) 88 } 89 90 func (n *navigation) CursorOnModeLine() bool { 91 return n.filtering 92 } 93 94 func (n *navigation) start(ed *editor) { 95 n.refresh() 96 ed.SetMode(n) 97 } 98 99 func (n *navigation) pageUp() { 100 n.current.pageUp() 101 n.refresh() 102 } 103 104 func (n *navigation) pageDown() { 105 n.current.pageDown() 106 n.refresh() 107 } 108 109 func (n *navigation) filePreviewUp() { 110 fp, ok := n.preview.(*navFilePreview) 111 if ok { 112 if fp.beginLine > 0 { 113 fp.beginLine-- 114 } 115 } 116 } 117 118 func (n *navigation) filePreviewDown() { 119 fp, ok := n.preview.(*navFilePreview) 120 if ok { 121 if fp.beginLine < len(fp.lines)-1 { 122 fp.beginLine++ 123 } 124 } 125 } 126 127 func (n *navigation) triggerShowHidden() { 128 n.showHidden = !n.showHidden 129 n.refresh() 130 } 131 132 func (n *navigation) triggerFilter() { 133 n.filtering = !n.filtering 134 } 135 136 func (n *navigation) insertSelected(ed *editor) { 137 ed.InsertAtDot(parse.Quote(n.current.selectedName()) + " ") 138 } 139 140 func (n *navigation) insertSelectedAndQuit(ed *editor) { 141 ed.InsertAtDot(parse.Quote(n.current.selectedName()) + " ") 142 ed.SetModeInsert() 143 } 144 145 func (n *navigation) defaultFn(ed *editor) { 146 // Use key binding for insert mode without exiting nigation mode. 147 k := ed.lastKey 148 if n.filtering && likeChar(k) { 149 n.filter += k.String() 150 n.refreshCurrent() 151 n.refreshDirPreview() 152 } else if n.filtering && k == (ui.Key{ui.Backspace, 0}) { 153 _, size := utf8.DecodeLastRuneInString(n.filter) 154 if size > 0 { 155 n.filter = n.filter[:len(n.filter)-size] 156 n.refreshCurrent() 157 n.refreshDirPreview() 158 } 159 } else { 160 fn := ed.insert.binding.GetOrDefault(k) 161 if fn == nil { 162 ed.Notify("key %s unbound and no default binding", k) 163 } else { 164 ed.CallFn(fn) 165 } 166 } 167 } 168 169 // Implementation. 170 // TODO(xiaq): Remember which file was selected in each directory. 171 172 var errorEmptyCwd = errors.New("current directory is empty") 173 174 func (n *navigation) maintainSelected(name string) { 175 n.current.selected = 0 176 for i, s := range n.current.candidates { 177 if s.Text > name { 178 break 179 } 180 n.current.selected = i 181 } 182 } 183 184 func (n *navigation) refreshCurrent() { 185 selectedName := n.current.selectedName() 186 all, err := n.loaddir(".") 187 if err != nil { 188 n.current = newErrNavColumn(err) 189 return 190 } 191 // Try to select the old selected file. 192 // XXX(xiaq): This would break when we support alternative ordering. 193 n.current = newNavColumn(all, func(i int) bool { 194 return i == 0 || all[i].Text <= selectedName 195 }) 196 n.current.changeFilter(n.filter) 197 n.maintainSelected(selectedName) 198 } 199 200 func (n *navigation) refreshParent() { 201 wd, err := os.Getwd() 202 if err != nil { 203 n.parent = newErrNavColumn(err) 204 return 205 } 206 if wd == "/" { 207 n.parent = newNavColumn(nil, nil) 208 } else { 209 all, err := n.loaddir("..") 210 if err != nil { 211 n.parent = newErrNavColumn(err) 212 return 213 } 214 cwd, err := os.Stat(".") 215 if err != nil { 216 n.parent = newErrNavColumn(err) 217 return 218 } 219 n.parent = newNavColumn(all, func(i int) bool { 220 d, _ := os.Lstat("../" + all[i].Text) 221 return os.SameFile(d, cwd) 222 }) 223 } 224 } 225 226 func (n *navigation) refreshDirPreview() { 227 if n.current.selected != -1 { 228 name := n.current.selectedName() 229 fi, err := os.Stat(name) 230 if err != nil { 231 n.preview = newErrNavColumn(err) 232 return 233 } 234 if fi.Mode().IsDir() { 235 all, err := n.loaddir(name) 236 if err != nil { 237 n.preview = newErrNavColumn(err) 238 return 239 } 240 n.preview = newNavColumn(all, func(int) bool { return false }) 241 } else { 242 n.preview = makeNavFilePreview(name) 243 } 244 } else { 245 n.preview = nil 246 } 247 } 248 249 // refresh rereads files in current and parent directories and maintains the 250 // selected file if possible. 251 func (n *navigation) refresh() { 252 n.refreshCurrent() 253 n.refreshParent() 254 n.refreshDirPreview() 255 } 256 257 // ascend changes current directory to the parent. 258 // TODO(xiaq): navigation.{ascend descend} bypasses the cd builtin. This can be 259 // problematic if cd acquires more functionality (e.g. trigger a hook). 260 func (n *navigation) ascend() error { 261 wd, err := os.Getwd() 262 if err != nil { 263 return err 264 } 265 if wd == "/" { 266 return nil 267 } 268 269 name := n.parent.selectedName() 270 err = os.Chdir("..") 271 if err != nil { 272 return err 273 } 274 n.filter = "" 275 n.refresh() 276 n.maintainSelected(name) 277 // XXX Refresh dir preview again. We should perhaps not have used refresh 278 // above. 279 n.refreshDirPreview() 280 return nil 281 } 282 283 // descend changes current directory to the selected file, if it is a 284 // directory. 285 func (n *navigation) descend() error { 286 if n.current.selected == -1 { 287 return errorEmptyCwd 288 } 289 name := n.current.selectedName() 290 err := n.chdir(name) 291 if err != nil { 292 return err 293 } 294 n.filter = "" 295 n.current.selected = -1 296 n.refresh() 297 n.refreshDirPreview() 298 return nil 299 } 300 301 // prev selects the previous file. 302 func (n *navigation) prev() { 303 if n.current.selected > 0 { 304 n.current.selected-- 305 } 306 n.refresh() 307 } 308 309 // next selects the next file. 310 func (n *navigation) next() { 311 if n.current.selected != -1 && n.current.selected < len(n.current.candidates)-1 { 312 n.current.selected++ 313 } 314 n.refresh() 315 } 316 317 func (n *navigation) loaddir(dir string) ([]ui.Styled, error) { 318 f, err := os.Open(dir) 319 if err != nil { 320 return nil, err 321 } 322 names, err := f.Readdirnames(-1) 323 if err != nil { 324 return nil, err 325 } 326 sort.Strings(names) 327 328 var all []ui.Styled 329 for _, name := range names { 330 if n.showHidden || name[0] != '.' { 331 all = append(all, ui.Styled{name, 332 ui.StylesFromString(path.Join(dir, name))}) 333 } 334 } 335 336 return all, nil 337 } 338 339 func (n *navigation) List(maxHeight int) ui.Renderer { 340 return makeNavRenderer( 341 maxHeight, 342 n.parent.FullWidth(maxHeight), 343 n.current.FullWidth(maxHeight), 344 n.preview.FullWidth(maxHeight), 345 n.parent.List(maxHeight), 346 n.current.List(maxHeight), 347 n.preview.List(maxHeight), 348 ) 349 } 350 351 // navColumn is a column in the navigation layout. 352 type navColumn struct { 353 listingMode 354 all []ui.Styled 355 candidates []ui.Styled 356 // selected int 357 err error 358 } 359 360 func newNavColumn(all []ui.Styled, sel func(int) bool) *navColumn { 361 nc := &navColumn{all: all, candidates: all} 362 nc.provider = nc 363 nc.selected = -1 364 for i := range all { 365 if sel(i) { 366 nc.selected = i 367 } 368 } 369 return nc 370 } 371 372 func newErrNavColumn(err error) *navColumn { 373 nc := &navColumn{err: err} 374 nc.provider = nc 375 return nc 376 } 377 378 func (nc *navColumn) Placeholder() string { 379 if nc.err != nil { 380 return nc.err.Error() 381 } 382 return "" 383 } 384 385 func (nc *navColumn) Len() int { 386 return len(nc.candidates) 387 } 388 389 func (nc *navColumn) Show(i int) (string, ui.Styled) { 390 cand := nc.candidates[i] 391 return "", ui.Styled{" " + cand.Text + " ", cand.Styles} 392 } 393 394 func (nc *navColumn) Filter(filter string) int { 395 nc.candidates = nc.candidates[:0] 396 for _, s := range nc.all { 397 if strings.Contains(s.Text, filter) { 398 nc.candidates = append(nc.candidates, s) 399 } 400 } 401 return 0 402 } 403 404 func (nc *navColumn) FullWidth(h int) int { 405 if nc == nil { 406 return 0 407 } 408 if nc.err != nil { 409 return util.Wcswidth(nc.err.Error()) 410 } 411 maxw := 0 412 for _, s := range nc.candidates { 413 maxw = max(maxw, util.Wcswidth(s.Text)+2) 414 } 415 if len(nc.candidates) > h { 416 maxw++ 417 } 418 return maxw 419 } 420 421 func (nc *navColumn) Accept(i int, ed eddefs.Editor) { 422 // TODO 423 } 424 425 func (nc *navColumn) ModeTitle(i int) string { 426 // Not used 427 return "" 428 } 429 430 func (nc *navColumn) selectedName() string { 431 if nc == nil || nc.selected == -1 || nc.selected >= len(nc.candidates) { 432 return "" 433 } 434 return nc.candidates[nc.selected].Text 435 }