github.com/hoop33/elvish@v0.0.0-20160801152013-6d25485beab4/edit/navigation.go (about) 1 package edit 2 3 import ( 4 "errors" 5 "os" 6 "path" 7 "strings" 8 "unicode/utf8" 9 10 "github.com/elves/elvish/parse" 11 ) 12 13 // Navigation subsystem. 14 15 // Interface. 16 17 type navigation struct { 18 current *navColumn 19 parent *navColumn 20 dirPreview *navColumn 21 showHidden bool 22 filtering bool 23 filter string 24 } 25 26 func (*navigation) Mode() ModeType { 27 return modeNavigation 28 } 29 30 func (n *navigation) ModeLine(width int) *buffer { 31 s := " NAVIGATING " 32 if n.showHidden { 33 s += "(show hidden) " 34 } 35 b := newBuffer(width) 36 b.writes(TrimWcWidth(s, width), styleForMode) 37 b.writes(" ", "") 38 b.writes(n.filter, styleForFilter) 39 b.dot = b.cursor() 40 return b 41 } 42 43 func startNav(ed *Editor) { 44 initNavigation(&ed.navigation) 45 ed.mode = &ed.navigation 46 } 47 48 func navUp(ed *Editor) { 49 ed.navigation.prev() 50 } 51 52 func navDown(ed *Editor) { 53 ed.navigation.next() 54 } 55 56 func navPageUp(ed *Editor) { 57 ed.navigation.current.pageUp() 58 } 59 60 func navPageDown(ed *Editor) { 61 ed.navigation.current.pageDown() 62 } 63 64 func navLeft(ed *Editor) { 65 ed.navigation.ascend() 66 } 67 68 func navRight(ed *Editor) { 69 ed.navigation.descend() 70 } 71 72 func navTriggerShowHidden(ed *Editor) { 73 ed.navigation.showHidden = !ed.navigation.showHidden 74 ed.navigation.refresh() 75 } 76 77 func navTriggerFilter(ed *Editor) { 78 ed.navigation.filtering = !ed.navigation.filtering 79 } 80 81 func navInsertSelected(ed *Editor) { 82 ed.insertAtDot(parse.Quote(ed.navigation.current.selectedName()) + " ") 83 } 84 85 func navigationDefault(ed *Editor) { 86 // Use key binding for insert mode without exiting nigation mode. 87 k := ed.lastKey 88 n := &ed.navigation 89 if n.filtering && likeChar(k) { 90 n.filter += k.String() 91 n.refreshCurrent() 92 } else if n.filtering && k == (Key{Backspace, 0}) { 93 _, size := utf8.DecodeLastRuneInString(n.filter) 94 if size > 0 { 95 n.filter = n.filter[:len(n.filter)-size] 96 n.refreshCurrent() 97 } 98 } else if f, ok := keyBindings[modeInsert][k]; ok { 99 f.Call(ed) 100 } else { 101 keyBindings[modeInsert][Default].Call(ed) 102 } 103 } 104 105 // Implementation. 106 // TODO(xiaq): Support file preview in navigation mode 107 // TODO(xiaq): Remember which file was selected in each directory. 108 109 var ( 110 errorEmptyCwd = errors.New("current directory is empty") 111 errorNoCwdInParent = errors.New("could not find current directory in ..") 112 ) 113 114 func initNavigation(n *navigation) { 115 *n = navigation{} 116 n.refresh() 117 } 118 119 func (n *navigation) maintainSelected(name string) { 120 n.current.selected = 0 121 for i, s := range n.current.candidates { 122 if s.text > name { 123 break 124 } 125 n.current.selected = i 126 } 127 } 128 129 func (n *navigation) refreshCurrent() { 130 selectedName := n.current.selectedName() 131 all, err := n.loaddir(".") 132 if err != nil { 133 n.current = newErrNavColumn(err) 134 return 135 } 136 // Try to select the old selected file. 137 // XXX(xiaq): This would break when we support alternative ordering. 138 n.current = newNavColumn(all, func(i int) bool { 139 return i == 0 || all[i].text <= selectedName 140 }) 141 n.current.changeFilter(n.filter) 142 n.maintainSelected(selectedName) 143 } 144 145 func (n *navigation) refreshParent() { 146 wd, err := os.Getwd() 147 if err != nil { 148 n.parent = newErrNavColumn(err) 149 return 150 } 151 if wd == "/" { 152 n.parent = newNavColumn(nil, nil) 153 } else { 154 all, err := n.loaddir("..") 155 if err != nil { 156 n.parent = newErrNavColumn(err) 157 return 158 } 159 cwd, err := os.Stat(".") 160 if err != nil { 161 n.parent = newErrNavColumn(err) 162 return 163 } 164 n.parent = newNavColumn(all, func(i int) bool { 165 d, _ := os.Lstat("../" + all[i].text) 166 return os.SameFile(d, cwd) 167 }) 168 } 169 } 170 171 func (n *navigation) refreshDirPreview() { 172 if n.current.selected != -1 { 173 name := n.current.selectedName() 174 fi, err := os.Stat(name) 175 if err != nil { 176 n.dirPreview = newErrNavColumn(err) 177 return 178 } 179 if fi.Mode().IsDir() { 180 all, err := n.loaddir(name) 181 if err != nil { 182 n.dirPreview = newErrNavColumn(err) 183 return 184 } 185 n.dirPreview = newNavColumn(all, func(int) bool { return false }) 186 } else { 187 // TODO(xiaq): Support regular file preview in navigation mode 188 n.dirPreview = nil 189 } 190 } else { 191 n.dirPreview = nil 192 } 193 } 194 195 // refresh rereads files in current and parent directories and maintains the 196 // selected file if possible. 197 func (n *navigation) refresh() { 198 n.refreshCurrent() 199 n.refreshParent() 200 n.refreshDirPreview() 201 } 202 203 // ascend changes current directory to the parent. 204 // TODO(xiaq): navigation.{ascend descend} bypasses the cd builtin. This can be 205 // problematic if cd acquires more functionality (e.g. trigger a hook). 206 func (n *navigation) ascend() error { 207 wd, err := os.Getwd() 208 if err != nil { 209 return err 210 } 211 if wd == "/" { 212 return nil 213 } 214 215 name := n.parent.selectedName() 216 err = os.Chdir("..") 217 if err != nil { 218 return err 219 } 220 n.filter = "" 221 n.refresh() 222 n.maintainSelected(name) 223 // XXX Refresh dir preview again. We should perhaps not have used refresh 224 // above. 225 n.refreshDirPreview() 226 return nil 227 } 228 229 // descend changes current directory to the selected file, if it is a 230 // directory. 231 func (n *navigation) descend() error { 232 if n.current.selected == -1 { 233 return errorEmptyCwd 234 } 235 name := n.current.selectedName() 236 err := os.Chdir(name) 237 if err != nil { 238 return err 239 } 240 n.filter = "" 241 n.current.selected = -1 242 n.refresh() 243 n.refreshDirPreview() 244 return nil 245 } 246 247 // prev selects the previous file. 248 func (n *navigation) prev() { 249 if n.current.selected > 0 { 250 n.current.selected-- 251 } 252 n.refresh() 253 } 254 255 // next selects the next file. 256 func (n *navigation) next() { 257 if n.current.selected != -1 && n.current.selected < len(n.current.candidates)-1 { 258 n.current.selected++ 259 } 260 n.refresh() 261 } 262 263 func (n *navigation) loaddir(dir string) ([]styled, error) { 264 f, err := os.Open(dir) 265 if err != nil { 266 return nil, err 267 } 268 infos, err := f.Readdir(0) 269 if err != nil { 270 return nil, err 271 } 272 var all []styled 273 for _, info := range infos { 274 if n.showHidden || info.Name()[0] != '.' { 275 name := info.Name() 276 all = append(all, styled{name, defaultLsColor.getStyle(path.Join(dir, name))}) 277 } 278 } 279 sortStyleds(all) 280 281 return all, nil 282 } 283 284 const ( 285 navigationListingColMargin = 1 286 navigationListingMinWidthForPadding = 5 287 ) 288 289 func (nav *navigation) List(width, maxHeight int) *buffer { 290 margin := navigationListingColMargin 291 var ratioParent, ratioCurrent, ratioPreview int 292 if nav.dirPreview != nil { 293 ratioParent = 15 294 ratioCurrent = 40 295 ratioPreview = 45 296 } else { 297 ratioParent = 15 298 ratioCurrent = 75 299 // Leave some space at the right side 300 } 301 302 w := width - margin*2 303 304 wParent := w * ratioParent / 100 305 wCurrent := w * ratioCurrent / 100 306 wPreview := w * ratioPreview / 100 307 308 b := renderNavColumn(nav.parent, wParent, maxHeight) 309 310 bCurrent := renderNavColumn(nav.current, wCurrent, maxHeight) 311 b.extendHorizontal(bCurrent, wParent+margin) 312 313 if wPreview > 0 { 314 bPreview := renderNavColumn(nav.dirPreview, wPreview, maxHeight) 315 b.extendHorizontal(bPreview, wParent+wCurrent+2*margin) 316 } 317 318 return b 319 } 320 321 // navColumn is a column in the navigation layout. 322 type navColumn struct { 323 listing 324 all []styled 325 candidates []styled 326 // selected int 327 err error 328 } 329 330 func newNavColumn(all []styled, sel func(int) bool) *navColumn { 331 nc := &navColumn{all: all, candidates: all} 332 nc.provider = nc 333 nc.selected = -1 334 for i := range all { 335 if sel(i) { 336 nc.selected = i 337 } 338 } 339 return nc 340 } 341 342 func newErrNavColumn(err error) *navColumn { 343 nc := &navColumn{err: err} 344 nc.provider = nc 345 return nc 346 } 347 348 func (nc *navColumn) Placeholder() string { 349 if nc.err != nil { 350 return nc.err.Error() 351 } 352 return "" 353 } 354 355 func (nc *navColumn) Len() int { 356 return len(nc.candidates) 357 } 358 359 func (nc *navColumn) Show(i, w int) styled { 360 s := nc.candidates[i] 361 if w >= navigationListingMinWidthForPadding { 362 return styled{" " + ForceWcWidth(s.text, w-2), s.style} 363 } 364 return styled{ForceWcWidth(s.text, w), s.style} 365 } 366 367 func (nc *navColumn) Filter(filter string) int { 368 nc.candidates = nc.candidates[:0] 369 for _, s := range nc.all { 370 if strings.Contains(s.text, filter) { 371 nc.candidates = append(nc.candidates, s) 372 } 373 } 374 return 0 375 } 376 377 func (nc *navColumn) Accept(i int, ed *Editor) { 378 // TODO 379 } 380 381 func (nc *navColumn) ModeTitle(i int) string { 382 // Not used 383 return "" 384 } 385 386 func (nc *navColumn) selectedName() string { 387 if nc == nil || nc.selected == -1 || nc.selected >= len(nc.candidates) { 388 return "" 389 } 390 return nc.candidates[nc.selected].text 391 } 392 393 func renderNavColumn(nc *navColumn, w, h int) *buffer { 394 return nc.List(w, h) 395 }