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