" Author:  Eric Van Dewoestine
"
" License: {{{
"
" Copyright (C) 2005 - 2017  Eric Van Dewoestine
"
" This program is free software: you can redistribute it and/or modify
" it under the terms of the GNU General Public License as published by
" the Free Software Foundation, either version 3 of the License, or
" (at your option) any later version.
"
" This program is distributed in the hope that it will be useful,
" but WITHOUT ANY WARRANTY; without even the implied warranty of
" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
" GNU General Public License for more details.
"
" You should have received a copy of the GNU General Public License
" along with this program.  If not, see <http://www.gnu.org/licenses/>.
"
" }}}

" Global Variables {{{
  if !exists("g:TreeDirHighlight")
    let g:TreeDirHighlight = "Statement"
  endif
  if !exists("g:TreeFileHighlight")
    let g:TreeFileHighlight = "Normal"
  endif
  if !exists("g:TreeFileExecutableHighlight")
    let g:TreeFileExecutableHighlight = "Constant"
  endif
  if !exists("g:TreeActionHighlight")
    let g:TreeActionHighlight = "Statement"
  endif
  if !exists('g:TreeExpandSingleDirs')
    let g:TreeExpandSingleDirs = 0
  endif
  if !exists('g:TreeIndent')
    let g:TreeIndent = 4
  endif
  if g:TreeIndent < 2
    call eclim#util#EchoWarning('g:TreeIndent cannot be less than 2.')
    let g:TreeIndent = 2
  endif
" }}}

" Script Variables {{{
  let s:node_prefix = ''
  let index = 0
  while index < (g:TreeIndent - 2)
    let s:node_prefix .= ' '
    let index += 1
  endwhile

  let s:dir_opened_prefix = '- '
  let s:dir_closed_prefix = '+ '
  let s:file_prefix = '  '

  let s:indent_length = len(s:node_prefix) + len(s:file_prefix)

  let s:node_regex = s:node_prefix .  '\(' .
    \ s:dir_opened_prefix . '\|' .
    \ s:dir_closed_prefix . '\|' .
    \ s:file_prefix . '\)'
  " \1 - indent, \2, node prefix + element prefix, \3 name
  let s:nodevalue_regex = '\(\s*\)' . s:node_regex . '\(.*\)'
  let s:root_regex = '^[/[:alpha:]]'

  let s:settings_loaded = 0

  let s:tree_count = 0
  let s:refresh_nesting = 0

  let s:vcol = 0
" }}}

function! eclim#tree#TreeHome() " {{{
  let name = "Tree"
  if s:tree_count > 0
    let name .= s:tree_count
  endif
  let s:tree_count += 1

  call eclim#tree#Tree(name, [eclim#UserHome()], [], 1, [])
endfunction " }}}

function! eclim#tree#TreePanes() " {{{
  call eclim#tree#TreeHome()
  vertical new
  call eclim#tree#TreeHome()
  1winc w
endfunction " }}}

function! eclim#tree#Tree(name, roots, aliases, expand, filters) " {{{
  " name - The name to use for the tree buffer.
  " roots - List of paths to use as tree roots.
  " aliases - List of aliases for root paths, or an empty list for no aliasing.
  " expand - 1 to pre expand the root directories, 0 otherwise.
  " filters - List of file name patterns to include in directory listings, or an
  "   empty list for no filtering.

  silent exec 'edit ' . escape(a:name, ' ')
  setlocal ft=tree
  setlocal nowrap
  setlocal noswapfile
  setlocal nobuflisted
  setlocal buftype=nofile
  setlocal bufhidden=wipe
  setlocal foldmethod=manual
  setlocal foldtext=getline(v:foldstart)
  setlocal sidescrolloff=0

  call s:Mappings()
  call eclim#tree#Syntax()

  " initialize autocmds before loading custom settings so that settings can
  " add autocmd events.
  augroup eclim_tree
    "autocmd! BufEnter,User <buffer>
    autocmd BufEnter <buffer> silent doautocmd eclim_tree User <buffer>
    exec 'autocmd BufDelete,BufUnload <buffer> ' .
      \ 'autocmd! eclim_tree * <buffer=' . bufnr('%') . '>'
  augroup END

  " register setting prior to listing any directories
  if exists("g:TreeSettingsFunction")
    let l:Settings = function(g:TreeSettingsFunction)
    call l:Settings()
    let s:settings_loaded = 1
  endif

  setlocal noreadonly modifiable
  silent 1,$delete _

  let roots = map(copy(a:roots), 'substitute(v:val, "\\([^/]\\)$", "\\1/", "")')
  let [roots, _] = s:NormalizeEntries(roots)
  let b:roots = copy(roots)
  let b:filters = a:filters
  let b:view_hidden = 0

  if len(a:aliases) > 0
    let b:aliases = {}
    let index = 0
    for alias in a:aliases
      if alias != ''
        let b:aliases[alias] = roots[index]
      endif
      let index += 1
    endfor

    call map(roots, 's:PathToAlias(v:val)')
  endif

  call append(line('$'), roots)

  if a:expand
    let index = len(roots)
    while index > 0
      call cursor(index + 1, 1)
      call eclim#tree#ExpandDir()
      let index = index - 1
    endwhile
  endif

  " delete empty first line.
  setlocal modifiable
  1,1delete _
  setlocal nomodifiable
endfunction " }}}

function! eclim#tree#ToggleCollapsedDir(Expand) " {{{
  if eclim#tree#GetPath() =~ '/$'
    if getline('.') =~ '\s*' . s:node_prefix . s:dir_closed_prefix ||
        \ (getline('.') =~ s:root_regex && eclim#tree#GetLastChildPosition() == line('.'))
      call a:Expand()
    else
      call s:CollapseDir()
    endif
  endif
endfunction " }}}

function! eclim#tree#ToggleFoldedDir(Expand) " {{{
  if eclim#tree#GetPath() =~ '/$'
    if foldclosed(line('.')) != -1
      call s:UnfoldDir()
    elseif getline('.') =~ '\s*' . s:node_prefix . s:dir_opened_prefix ||
        \ (getline('.') =~ s:root_regex && eclim#tree#GetLastChildPosition() != line('.'))
      call s:FoldDir()
    else
      call a:Expand()
    endif
  endif
endfunction " }}}

function! eclim#tree#ToggleViewHidden() " {{{
  let b:view_hidden = (b:view_hidden + 1) % 2

  let line = getline('.')
  let path = eclim#tree#GetPath()
  call cursor(1, 1)
  call eclim#tree#Refresh()
  while search(s:root_regex, 'W') != 0
    call eclim#tree#Refresh()
  endwhile

  call cursor(1, 1)
  while search(line, 'W') != 0 && eclim#tree#GetPath() != path
  endwhile
  call eclim#tree#Cursor(line, 0)
endfunction " }}}

function! eclim#tree#GetFileInfo(file) " {{{
  if executable('ls')
    return split(eclim#util#System("ls -ld '" . a:file . "'"), '\n')[0]
  endif
  return ''
endfunction "}}}

function! eclim#tree#GetPath(...) " {{{
  " Optional args:
  "   resolve_links
  let line = getline('.')
  let node = substitute(line, s:nodevalue_regex, '\3', '')

  let node = eclim#tree#GetParent(a:0 ? a:1 : 1) . node
  let path = s:AliasToPath(node)

  " handle symbolic links
  if path =~ '->'
    if !a:0 || a:1 " resolve links
      let link = substitute(path, '.* -> \(.*\)', '\1', '')
      if link !~ '^/' && link !~ '^[a-zA-Z]:/'
        let parent = substitute(path, '\(.*\) -> .*', '\1', '')
        let parent = fnamemodify(substitute(parent, '/$', '', ''), ':h')
        let link = parent . '/' . link
      endif
      let path = link
    else
      let path = substitute(path, '\(.*\) -> .*', '\1', '')
      if node =~ '/$'
        let path .= '/'
      endif
    endif
  endif

  " handle executable files.
  if path =~ '\*$'
    let path = strpart(path, 0, len(path) - 1)
  endif

  return path
endfunction "}}}

function! eclim#tree#GetParent(...) " {{{
  " Optional args:
  "   resolve_links
  let parent = ''

  let lnum = eclim#tree#GetParentPosition()
  if lnum
    let pos = getpos('.')
    call cursor(lnum, 1)
    let parent = eclim#tree#GetPath(a:0 ? a:1 : 1)
    call setpos('.', pos)
  endif

  return parent
endfunction " }}}

function! eclim#tree#GetParentPosition() " {{{
  let lnum = 0
  let line = getline('.')
  if line =~ '\s*' . s:node_prefix
    if line =~ '^' . s:node_regex . '\S'
      let search = s:root_regex
    else
      let search = '^'
      let index = 0
      let indent = s:GetIndent(line('.'))
      while index < indent - s:indent_length
        let search .= ' '
        let index += 1
      endwhile
      let search .= s:node_prefix .  s:dir_opened_prefix
    endif

    let lnum = search(search, 'bnW')

    " hack: most likely one of:
    "   - refresh from comment line
    "   - refresh from first level dir/file with g:TreeIndent < 4
    if lnum == 0 && search != s:root_regex
      let lnum = search(s:root_regex, 'bnW')
    endif
  endif

  return lnum
endfunction " }}}

function! eclim#tree#GetLastChildPosition() " {{{
  let line = getline('.')

  " a root node
  if line =~ s:root_regex
    let lnum = search(s:root_regex, 'nW')
    return lnum > 0 ? lnum  - 1 : s:GetLastLine()
  endif

  " non root node
  let sibling = '^' .
    \ substitute(line, s:nodevalue_regex, '\1' . escape(s:node_regex. '[.[:alnum:]_]', '\'), '')
  let lnum = line('.') + 1
  let indent = s:GetIndent(line('.'))
  while getline(lnum) !~ sibling &&
      \ s:GetIndent(lnum) >= indent &&
      \ lnum != s:GetLastLine()
    let lnum += 1
  endwhile

  " back up one if on a node of equal or less depth
  if s:GetIndent(lnum) <= indent
    let lnum -= 1
  endif

  " no sibling below, use parent's value
  if lnum == line('.') && getline(lnum + 1) !~ sibling
    let pos = getpos('.')

    call cursor(eclim#tree#GetParentPosition(), 1)
    let lnum = eclim#tree#GetLastChildPosition()

    call setpos('.', pos)
  endif

  return lnum
endfunction " }}}

function! eclim#tree#Execute(alt) " {{{
  if getline('.') =~ '^"\|^\s*$'
    return
  endif

  let path = eclim#tree#GetPath()

  " execute action on dir
  if path =~ '/$'
    if a:alt || foldclosed(line('.')) != -1
      call eclim#tree#ToggleFoldedDir(function('eclim#tree#ExpandDir'))
    else
      call eclim#tree#ToggleCollapsedDir(function('eclim#tree#ExpandDir'))
    endif

  " execute action on file
  else
    if !filereadable(path)
      echo "File is not readable or has been deleted."
    endif

    let actions = eclim#tree#GetFileActions(path)
    if len(actions) == 0
      echo "No registered actions for file: " . path
      return
    endif

    if a:alt
      call eclim#tree#DisplayActionChooser(
        \ path, actions, 'eclim#tree#ExecuteAction')
    else
      call eclim#tree#ExecuteAction(path, actions[0].action)
    endif
  endif
endfunction " }}}

function! eclim#tree#ExecuteAction(file, command) " {{{
  let file = eclim#util#Simplify(a:file)
  let file = escape(file, ' &()')
  let file = escape(file, ' &()') " need to double escape
  let file = escape(file, '&') " '&' needs to be escaped 3 times.

  let command = a:command
  let command = substitute(command, '<file>', file, 'g')
  if command =~ '^!\w'
    silent call eclim#util#Exec(command)
    redraw!
  else
    exec command
  endif

  if command =~ '^!\w' && v:shell_error
    call eclim#util#EchoError('Error executing command: ' . command)
  endif
endfunction " }}}

function! eclim#tree#RegisterFileAction(regex, name, action) " {{{
  " regex - Pattern to match the file name against.
  " name - Name of the action used for display purposes.
  " action - The action to execute where <file> is replaced with the filename.

  if !exists('b:file_actions')
    let b:file_actions = []
  endif

  let entry = {}
  for e in b:file_actions
    if e.regex == a:regex
      let entry = e
      break
    endif
  endfor

  if len(entry) == 0
    let entry = {'regex': a:regex, 'actions': []}
    call add(b:file_actions, entry)
  endif

  call add(entry.actions, {'name': a:name, 'action': a:action})
endfunction " }}}

function! eclim#tree#RegisterDirAction(action) " {{{
  " action - A funcref which will be invoked when expanding a directory with the
  " directory path and a mutable list of current directory contents.
  if !exists('b:dir_actions')
    let b:dir_actions = []
  endif
  call add(b:dir_actions, a:action)
endfunction " }}}

function! eclim#tree#GetFileActions(file) " {{{
  " Returns a list of dictionaries with keys 'name' and 'action'.
  let actions = []
  let thefile = tolower(a:file)
  let bufnr = bufnr('%')
  for entry in b:file_actions
    if thefile =~ entry.regex
      let actions += entry.actions
    endif
  endfor

  return actions
endfunction " }}}

function! eclim#tree#Shell(external) " {{{
  " Opens a shell either in the current vim session or externally.
  let path = eclim#tree#GetPath()
  if !isdirectory(path)
    let path = fnamemodify(path, ':h')
  endif

  let cwd = getcwd()
  silent exec "lcd " . escape(path, ' &')
  if a:external
    if !exists("g:TreeExternalShell")
      echo "No external shell configured via 'g:TreeExternalShell' variable."
    else
      silent call eclim#util#Exec(g:TreeExternalShell)
      redraw!
    endif
  else
    shell
  endif
  silent exec "lcd " . escape(cwd, ' &')
endfunction " }}}

function! eclim#tree#Cursor(line, prevline) " {{{
  let lnum = a:line
  let line = getline(lnum)

  if line =~ s:root_regex
    call cursor(lnum, 1)
  else
    " get the starting column of the current line and the previous line
    let start = len(line) - len(substitute(line, '^\s\+\W', '', ''))

    " only use the real previous line if we've only moved one line
    let moved = a:prevline - lnum
    if moved < 0
      let moved = -moved
    endif
    let pline = moved == 1 ? getline(a:prevline) : ''
    let pstart = pline != '' ?
      \ len(pline) - len(substitute(pline, '^\s\+\W', '', '')) : -1

    " only change the cursor column if the hasn't user has moved it to the
    " right to view more of the entry
    let cnum = start == pstart ? 0 : start
    call cursor(lnum, cnum)

    " attempt to maximize the amount of text on the current line that is in
    " view, but only if we've changed column position
    let winwidth = winwidth(winnr())
    let vcol = exists('s:vcol') ? s:vcol : 0
    let col = col('.')
    if cnum != 0 && (!vcol || ((len(line) - vcol) > winwidth))
      if len(line) > winwidth
        normal! zs
        " scroll back enough to keep the start of the parent in view
        normal! 6zh
        let s:vcol = col - 6
      endif
    endif

    " when the text view is shifted by vim it appears to always shift back one
    " half of the window width, so recalculate our visible column accordingly
    " if we detect such a shift... may not always be accurate.
    if s:vcol > col
      let s:vcol = max([start - (winwidth / 2), 0])
    endif
  endif
endfunction " }}}

function! eclim#tree#GetRoot() " {{{
  if getline('.') =~ s:root_regex
    return s:AliasToPath(getline('.'))
  endif
  let start = search(s:root_regex, 'bcnW')
  return s:AliasToPath(getline(start))
endfunction " }}}

function! eclim#tree#SetRoot(path) " {{{
  let path = s:AliasToPath(a:path)
  let path = s:NormalizeEntries([fnamemodify(path, ':p')])[0][0]
  if !isdirectory(path)
    echo 'Directory does not exist or may have been deleted.'
    return
  endif

  let path = s:PathToAlias(path)

  " if on a root node
  if getline('.') =~ s:root_regex
    let start = line('.')

  " not on a root node
  else
    let start = search(s:root_regex, 'bW')
  endif
  let end = eclim#tree#GetLastChildPosition()

  setlocal noreadonly modifiable
  silent exec start . ',' . end . 'delete _'

  let line = line('.')
  if line == 1
    let line = 0
  endif
  call append(line, path)

  " delete blank first line if any
  if getline(1) =~ '^$'
    silent 1,1delete _
  endif
  " delete blank last line if any
  if getline('$') =~ '^$'
    silent exec line('$') . ',' . line('$') . 'delete _'
  endif

  call cursor(line + 1, 1)
  call eclim#tree#ExpandDir()
  setlocal nomodifiable
endfunction " }}}

function! eclim#tree#Refresh() " {{{
  " FIXME: in need of a serious rewrite (probably need to rewrite the whole
  " plugin)

  let ignore_pattern = ''
  if &wildignore != ''
    let ignore_pattern = substitute(escape(&wildignore, '.'), '\*', '.*', 'g')
    let ignore_pattern = '\(' . join(split(ignore_pattern, ','), '\|') . '\)$'
  endif

  let clnum = line('.')
  let ccnum = col('.')

  let startpath = eclim#tree#GetPath()
  if s:refresh_nesting == 0
    let s:startpath = startpath
    " let vim track shifts in line numbers with a mark
    mark Z
  endif

  " if on a file or closed directory, refresh it's parent
  if startpath !~ '/$' ||
      \ getline('.') =~ '^\s*' . s:node_prefix . s:dir_closed_prefix
    call cursor(eclim#tree#GetParentPosition(), 1)
    let startpath = eclim#tree#GetPath()
  endif

  let start = line('.')
  let end = eclim#tree#GetLastChildPosition()

  " first check the node we are on
  if (!isdirectory(startpath) && !filereadable(startpath)) ||
      \ (getline('.') !~ s:root_regex && s:IsHidden(startpath, ignore_pattern))
    setlocal modifiable
    silent exec start . ',' . end . 'delete _'
    setlocal nomodifiable
    silent doautocmd eclim_tree User <buffer>
    return
  endif

  if s:refresh_nesting == 0
    call eclim#util#Echo('Refreshing...')
  endif
  let s:refresh_nesting += 1

  " move cursor to first child
  call cursor(start + 1, 1)
  " get pattern to use to match children.
  let match = substitute(getline('.'), '^' . s:nodevalue_regex, '\1', '')
  let match = '^' . match . s:node_regex . '[.[:alnum:]_]'

  " walk the tree
  let lnum = line('.')
  while lnum <= end && lnum <= s:GetLastLine()

    let line = getline('.')

    " open dir that needs to be refreshed as well.
    if line =~ '\s*' . s:node_prefix . s:dir_opened_prefix
      call eclim#tree#Refresh()
      let lnum = eclim#tree#GetLastChildPosition()
      let ldiff = lnum - line('.')
      let end += ldiff
      call cursor(lnum, 1)
    endif

    let path = eclim#tree#GetPath()

    " delete files, and dirs that do not exist, or are hidden.
    if (path =~ '/$' && !isdirectory(path)) ||
     \ (path !~ '/$' && !filereadable(path)) ||
     \ s:IsHidden(path, ignore_pattern)
      let last = eclim#tree#GetLastChildPosition()
      setlocal modifiable
      silent exec lnum . ',' . last . 'delete _'
      setlocal nomodifiable
      let end -= (last - lnum) + 1
      continue
    endif

    let lnum += 1
    call cursor(lnum, 1)
  endwhile
  call cursor(start + 1, ccnum)

  " merge in any dirs that have been added
  let contents = eclim#tree#ListDir(startpath)
  let [dirs, files] = s:NormalizeEntries(contents)
  let contents = dirs + files
  let root = eclim#tree#GetRoot()
  let indent = eclim#tree#GetChildIndent(start)
  let lnum = line('.')
  setlocal modifiable
  for entry in contents
    let path = eclim#tree#GetPath(0)
    let path_link = eclim#tree#GetPath(0)
    let rewrote = s:RewriteSpecial(entry)
    let norm_entry = substitute(entry, '[*@]$', '', '')
    if rewrote =~ '/$' && norm_entry !~ '/$'
      let norm_entry .= '/'
    endif

    " ugly
    if exists('b:links')
      for [link, target] in items(b:links)
        if path =~ '^' . root . link
          let path = substitute(path, root . link, target, '')
          break
        endif
      endfor
    endif

    if path != norm_entry && path_link != norm_entry
      " if we are adding a new entry we'll just add one that has the correct
      " index + prefix and let the next block set the proper display path.
      if s:MatchesFilter(norm_entry)
        if isdirectory(entry)
          let initial = fnamemodify(substitute(entry, '/$', '', ''), ':t') . '/'
        else
          let initial = fnamemodify(entry, ':t')
        endif

        if index(dirs, entry) != -1
          let display_entry = indent . s:node_prefix . s:dir_closed_prefix . initial
        else
          let display_entry = indent . s:node_prefix . s:file_prefix . initial
        endif
        if lnum <= s:GetLastLine()
          call append(lnum - 1, display_entry)
        else
          call append(s:GetLastLine(), display_entry)
        endif
        call cursor(lnum, 0)
      endif
    endif

    let parent = eclim#tree#GetParent(0)
    let parent_link = eclim#tree#GetParent()
    if index(dirs, entry) != -1
      let dir_prefix = s:dir_closed_prefix
      if getline(lnum) =~ '\s*' . s:node_prefix . s:dir_opened_prefix
        let dir_prefix = s:dir_opened_prefix
      endif
      let display_entry = indent . s:node_prefix . dir_prefix .
        \ substitute(substitute(rewrote, parent, '', ''), parent_link, '', '')
    else
      let display_entry = indent . s:node_prefix . s:file_prefix .
        \ substitute(substitute(rewrote, parent, '', ''), parent_link, '', '')
    endif

    call setline(lnum, display_entry)
    if getline(lnum) =~ '\s*' . s:node_prefix . s:dir_opened_prefix
      call cursor(eclim#tree#GetLastChildPosition() + 1, 1)
      let lnum = line('.')
    else
      let lnum += 1
      call cursor(lnum, 1)
    endif

  endfor
  setlocal nomodifiable

  call cursor(clnum, ccnum)
  let s:refresh_nesting -= 1

  if s:refresh_nesting == 0
    call eclim#util#Echo(' ')
    " return to marked position.
    call cursor(line("'Z"), col("`Z"))
    " if the entry that we started on is gone, move the cursor up a line.
    if s:startpath != eclim#tree#GetPath()
      call cursor(line('.') - 1, col('.'))
    endif
  endif
  silent doautocmd eclim_tree User <buffer>
endfunction " }}}

function! eclim#tree#MoveToLastChild() " {{{
  mark '
  if getline('.') !~ '^\s*' . s:node_prefix . s:dir_opened_prefix . '[.[:alnum:]_]'
    call cursor(eclim#tree#GetParentPosition(), 1)
  endif
  call eclim#tree#Cursor(eclim#tree#GetLastChildPosition(), 0)
endfunction " }}}

function! eclim#tree#MoveToParent() " {{{
  mark '
  call eclim#tree#Cursor(eclim#tree#GetParentPosition(), 0)
endfunction " }}}

function! eclim#tree#Mkdir() " {{{
  let path = eclim#tree#GetPath()
  if !isdirectory(path)
    let path = fnamemodify(path, ':h') . '/'
  endif

  let response = input('mkdir: ', path, 'dir')
  if response == '' || response == path
    return
  endif

  " work around apparent vim bug attempting to create a dir with a trailing
  " slash.
  if response[-1:] == '/'
    let response = response[:-2]
  endif

  call mkdir(response, 'p')
  call eclim#tree#Refresh()
endfunction " }}}

function! s:AliasToPath(alias) " {{{
  if exists('b:aliases')
    let alias = ''
    for alias in keys(b:aliases)
      if alias != '' && a:alias =~ '^' . alias . '\>/'
        return substitute(a:alias, '^' . alias . '/', b:aliases[alias], '')
      endif
    endfor
  endif
  return a:alias
endfunction " }}}

function! s:PathToAlias(path) " {{{
  if exists('b:aliases')
    let path = ''
    for alias in keys(b:aliases)
      let path = b:aliases[alias]
      if alias != '' && a:path =~ '^' . path
        return substitute(a:path, '^' . path, alias . '/', '')
      endif
    endfor
  endif
  return a:path
endfunction " }}}

function! s:Depth() " {{{
  return len(split(eclim#tree#GetPath(), '/'))
endfunction " }}}

function! eclim#tree#ExpandDir() " {{{
  let dir = eclim#tree#GetPath()

  if !isdirectory(dir)
    echo "Not a directory or directory may have been removed."
    return
  endif

  let contents = eclim#tree#ListDir(dir)
  let [dirs, files] = s:NormalizeEntries(contents)

  call map(dirs, 'substitute(v:val, "@$", "", "")')
  call map(files, 'substitute(v:val, "@$", "", "")')

  " filter files
  let filtered = []
  for file in files
    if s:MatchesFilter(file)
      call add(filtered, file)
    endif
  endfor
  let files = filtered

  " rewrite any special files (executables, symbolic links, etc).
  call map(dirs, 's:RewriteSpecial(v:val)')
  call map(files, 's:RewriteSpecial(v:val)')

  call eclim#tree#WriteContents(dir, dirs, files)
  if g:TreeExpandSingleDirs && len(files) == 0 && len(dirs) == 1 && s:Depth() < 50
    TreeNextPrevLine j
    call eclim#tree#ExpandDir()
  endif
endfunction " }}}

function! eclim#tree#ExpandPath(name, path) " {{{
  " Given the buffer name of a tree and a full path in that tree, either with an
  " alias or real root path at the beginning, expand the tree node to reveal
  " that path.

  let winnr = winnr()
  let treewin = bufwinnr(a:name)
  if treewin == -1
    return
  endif

  exec treewin . 'winc w'

  let path = a:path
  let root = ''
  for r in b:roots
    let r = substitute(r, '/$', '', '')
    if path =~ '^' . r . '\>'
      let root = r
      break
    endif
  endfor

  " try aliases
  if root == ''
    let path = substitute(path, '^/', '', '')
    for r in keys(b:aliases)
      if path =~ '^' . r . '\>'
        let root = r
        break
      endif
    endfor
  endif

  if root != ''
    let path = substitute(path, '^' . root, '', '')
    call cursor(1, 1)
    for dir in split(path, '/')
      call eclim#tree#MoveToLastChild()
      let line = search('[+-] \<' . dir . '\>/', 'nbW')
      if line
        call eclim#tree#Cursor(line, 0)
        if getline(line) =~ '^\s*+'
          call eclim#tree#Execute(1)
        endif
      else
        break
      endif
    endfor
  endif

  exec winnr . 'winc w'
endfunction " }}}

function! eclim#tree#WriteContents(dir, dirs, files) " {{{
  let dirs = a:dirs
  let files = a:files
  let indent = eclim#tree#GetChildIndent(line('.'))
  call map(dirs,
    \ 'substitute(v:val, a:dir, indent . s:node_prefix . s:dir_closed_prefix, "")')
  call map(files,
    \ 'substitute(v:val, a:dir, indent . s:node_prefix . s:file_prefix, "")')

  " update current line
  call s:UpdateLine(s:node_prefix . s:dir_closed_prefix,
    \ s:node_prefix . s:dir_opened_prefix)

  setlocal noreadonly modifiable
  let content = dirs + files
  call append(line('.'), content)
  setlocal nomodifiable
  return content
endfunction " }}}

function! s:RewriteSpecial(file) " {{{
  let file = a:file
  let info = ''
  let file = substitute(file, '@$', '', '')

  " symbolic links
  let tmpfile = file =~ '/$' ? strpart(file, 0, len(file) - 1) : file
  if getftype(tmpfile) == 'link'
    if info == ''
      let info = eclim#util#System('ls -ldF ' . tmpfile)
    endif
    let linkto = substitute(info, '.*-> \(.*\)\n', '\1', '')

    if linkto =~ '//$'
      let linkto = strpart(linkto, 0, len(linkto) - 1)
    endif

    let file = tmpfile . ' -> ' . linkto
  endif

  if exists('b:links')
    if file =~ '/$'
      let path = substitute(file, '/$', '', '')
      let entry = substitute(path, eclim#tree#GetRoot(), '', '')
      if has_key(b:links, entry)
        let file = path . ' -> ' . get(b:links, entry)
        let file = substitute(file, '\([^/]\)$', '\1/', '')
      endif
    endif
  endif

  return file
endfunction " }}}

function! s:CollapseDir() " {{{
  " update current line
  call s:UpdateLine(s:node_prefix . s:dir_opened_prefix,
    \ s:node_prefix . s:dir_closed_prefix)

  let lnum = line('.')
  let cnum = col('.')
  let start = lnum + 1
  let end = eclim#tree#GetLastChildPosition()

  if start > end
    return
  endif

  setlocal noreadonly modifiable
  silent exec start . ',' . end . 'delete _'
  setlocal nomodifiable

  call cursor(lnum, cnum)
endfunction " }}}

function! s:UnfoldDir() " {{{
  foldopen
endfunction " }}}

function! s:FoldDir() " {{{
  let start = line('.')
  let end = eclim#tree#GetLastChildPosition()

  exec start . ',' . end . 'fold'
endfunction " }}}

function! eclim#tree#ListDir(dir, ...) " {{{
  " Optional args:
  "   execute_actions
  let ls = 'ls -1F'
  if b:view_hidden
    let ls .= 'A'
  endif
  let contents = split(eclim#util#System(ls . " '" . a:dir . "'"), '\n')
  if !b:view_hidden && &wildignore != ''
    let pattern = substitute(escape(&wildignore, '.~'), '\*', '.*', 'g')
    let pattern = '\(' . join(split(pattern, ','), '\|') . '\)$'
    " Note: symlinks have a trailing @, so remove that before comparing
    " against pattern
    call filter(contents, 'substitute(v:val, "@$", "", "") !~ pattern')
  endif
  call map(contents, 'a:dir . v:val')

  if exists('b:dir_actions') && (!a:0 || a:1)
    for l:Action in b:dir_actions
      call l:Action(a:dir, contents)
    endfor
  endif

  return contents
endfunction " }}}

function! s:GetIndent(line) " {{{
  let indent = indent(a:line)
  if getline(a:line) =~ s:file_prefix . '[.[:alnum:]_]' && s:file_prefix =~ '^\s*$'
    let indent -= len(s:file_prefix)
  endif
  if s:node_prefix =~ '^\s*$'
    let indent -= len(s:node_prefix)
  endif

  return indent
endfunction " }}}

function! s:GetLastLine() " {{{
  let line = line('$')
  while getline(line) =~ '^"\|^\s*$' && line > 1
    let line -= 1
  endwhile
  return line
endfunction " }}}

function! eclim#tree#GetChildIndent(line) " {{{
  let indent = ''
  if getline(a:line) =~ '\s*' . s:node_prefix
    let num = indent(a:line)

    if s:node_prefix =~ '^\s*$'
      let num -= len(s:node_prefix)
    endif

    let index = 0
    while index < num + s:indent_length
      let indent .= ' '
      let index += 1
    endwhile
  endif

  return indent
endfunction " }}}

function! s:MatchesFilter(file) " {{{
  if len(b:filters) > 0
    for filter in b:filters
      if entry =~ filter
        return 1
      endif
    endfor
    return 0
  endif

  return 1
endfunction " }}}

function! s:IsHidden(path, ignore_pattern) " {{{
  if !b:view_hidden
    let path = a:path
    if isdirectory(path)
      let path = fnamemodify(path, ':h')
    endif
    let path = fnamemodify(path, ':t')
    return path =~ '^\.' || (a:ignore_pattern != '' && path =~ a:ignore_pattern)
  endif
  return 0
endfunction " }}}

function! s:NormalizeEntries(dirs) " {{{
  " normalize path separators
  call map(a:dirs, 'substitute(v:val, "\\\\", "/", "g")')

  let dirs = filter(copy(a:dirs),
    \ 'v:val =~ "/$" || (v:val =~ "@$" && isdirectory(substitute(v:val, "@$", "", "")))')
  let files = filter(copy(a:dirs), 'index(dirs, v:val) == -1')

  return [dirs, files]
endfunction " }}}

function! s:UpdateLine(pattern, substitution) " {{{
  let lnum = line('.')
  let line = getline(lnum)
  let line = substitute(line, a:pattern, a:substitution, '')

  setlocal noreadonly modifiable
  call append(lnum, line)
  silent exec lnum . ',' . lnum . 'delete _'
  setlocal nomodifiable
endfunction " }}}

function! eclim#tree#DisplayActionChooser(file, actions, executeFunc) " {{{
  keepalt new
  let height = len(a:actions) + 1

  exec 'resize ' . height

  setlocal noreadonly modifiable
  let b:actions = a:actions
  let b:file = a:file
  for action in a:actions
    call append(line('$'), action.name)
  endfor

  exec 'nnoremap <buffer> <silent> <cr> ' .
    \ ':call eclim#tree#ActionExecute("' . a:executeFunc . '")<cr>'
  nnoremap <buffer> q :q<cr>

  exec "hi link TreeAction " . g:TreeActionHighlight
  syntax match TreeAction /.*/

  1,1delete _
  setlocal nomodifiable
  setlocal noswapfile
  setlocal buftype=nofile
  setlocal bufhidden=wipe
endfunction "}}}

function! eclim#tree#ActionExecute(executeFunc) " {{{
  let command = ''
  let line = getline('.')
  for action in b:actions
    if action.name == line
      let command = action.action
      break
    endif
  endfor

  let file = b:file
  close
  call function(a:executeFunc)(file, command)
endfunction "}}}

function! s:Mappings() " {{{
  nnoremap <buffer> <silent> <cr> :call eclim#tree#Execute(0)<cr>
  nnoremap <buffer> <silent> o    :call eclim#tree#Execute(1)<cr>

  nnoremap <buffer> <silent> i    :call eclim#util#Echo(
    \ eclim#tree#GetFileInfo(eclim#tree#GetPath()))<cr>
  nnoremap <buffer> <silent> I    :call eclim#util#Echo(
    \ eclim#tree#GetFileInfo(eclim#tree#GetPath()))<cr>

  nnoremap <buffer> <silent> s    :call eclim#tree#Shell(0)<cr>
  nnoremap <buffer> <silent> S    :call eclim#tree#Shell(1)<cr>

  nnoremap <buffer> <silent> R    :call eclim#tree#Refresh()<cr>

  nnoremap <buffer> <silent> A    :call eclim#tree#ToggleViewHidden()<cr>

  nnoremap <buffer> <silent> ~    :call eclim#tree#SetRoot(eclim#UserHome())<cr>
  nnoremap <buffer> <silent> C    :call eclim#tree#SetRoot(eclim#tree#GetPath())<cr>
  nnoremap <buffer> <silent> K    :call eclim#tree#SetRoot(substitute(
    \ <SID>PathToAlias(eclim#tree#GetRoot()),
    \ '^\([^/]*/\).*', '\1', ''))<cr>
  nnoremap <buffer> <silent> B    :call eclim#tree#SetRoot(
    \ fnamemodify(eclim#tree#GetRoot(), ':h:h'))<cr>

  nnoremap <buffer> <silent> j    :TreeNextPrevLine j<cr>
  nnoremap <buffer> <silent> k    :TreeNextPrevLine k<cr>
  nnoremap <buffer> <silent> p    :call eclim#tree#MoveToParent()<cr>
  nnoremap <buffer> <silent> P    :call eclim#tree#MoveToLastChild()<cr>

  nnoremap <buffer> <silent> D    :call eclim#tree#Mkdir()<cr>

  let ctrl_l = escape(maparg('<c-l>'), '|')
  exec 'nnoremap <buffer> <silent> <c-l> :silent doautocmd eclim_tree User <buffer><cr>' . ctrl_l

  command! -nargs=1 -complete=dir -buffer CD :call eclim#tree#SetRoot('<args>')
  command! -nargs=1 -complete=dir -buffer Cd :call eclim#tree#SetRoot('<args>')

  " only needed as a command to support counts on the j/k mappings
  command! -nargs=? -count=1 -buffer TreeNextPrevLine
    \ let c = <count> |
    \ let c = c > 1 ? c - line('.') + 1 : c |
    \ let prev = line('.') |
    \ exec 'normal! ' . c . '<args>' |
    \ call eclim#tree#Cursor(line('.'), prev)
endfunction " }}}

function! eclim#tree#Syntax() " {{{
  exec "hi link TreeDir " . g:TreeDirHighlight
  exec "hi link TreeFile " . g:TreeFileHighlight
  exec "hi link TreeFileExecutable " . g:TreeFileExecutableHighlight
  hi link TreeMarker Normal
  syntax match TreeMarker /^\s*[-+]/
  syntax match TreeDir /\S.*\// contains=TreeMarker
  syntax match TreeFile /\S.*[^\/]$/
  syntax match TreeFileExecutable /\S.*[^\/]\*$/
  syntax match Comment /^".*/
endfunction " }}}

" vim:ft=vim:fdm=marker
