├── README.md ├── bin └── filemru.sh └── plugin └── fzf-filemru.vim /README.md: -------------------------------------------------------------------------------- 1 | # FZF File MRU 2 | 3 | Vim plugin that tracks your most recently and frequently used files 4 | while using the [fzf.vim](https://github.com/junegunn/fzf.vim) plugin. 5 | 6 | ![Sweet FZF MRU GIF](https://cloud.githubusercontent.com/assets/111942/14758993/2dcf6748-08e0-11e6-9b0a-3f4d33d5c87c.gif) 7 | 8 | This plugin provides the `FilesMru` and `ProjectMru` commands, which are 9 | basically a pass-throughs to the `Files` command. So, all you really need to 10 | do is use `FilesMru` instead of `Files`. 11 | 12 | When using `FilesMru` or `ProjectMru`, FZF will display files like usual, 13 | except your most recently used files (matching the working directory) will 14 | appear before all other files. 15 | 16 | `ProjectMru` does the same thing as `FilesMru` except that it uses 17 | `git ls-tree` to display files after MRU files (and before other found files), 18 | and ignores repository submodule directories. 19 | 20 | `UpdateMru` is a utility command that allows you to manually update the MRU. 21 | 22 | MRU files are tracked in `$XDG_CACHE_HOME/fzf_filemru`. A timestamp (rounded 23 | to 2 minute intervals) and selection count is used to determine recency and 24 | frequency. 25 | 26 | 27 | ## Example Usage 28 | 29 | ```vim 30 | nnoremap :FilesMru --tiebreak=end 31 | ``` 32 | 33 | The MRU list is updated when a file is saved or selected from the FZF menu. 34 | Though not recommended, you could update the MRU list when a file is opened by 35 | other means with the following script: 36 | 37 | ```vim 38 | augroup custom_filemru 39 | autocmd! 40 | autocmd BufWinEnter * UpdateMru 41 | augroup END 42 | ``` 43 | 44 | 45 | ## Requirements 46 | 47 | - [fzf.vim](https://github.com/junegunn/fzf.vim) 48 | - bash 49 | - awk 50 | - GNU or MacOS `date` (supporting the `%s` format option) 51 | 52 | 53 | # Command Usage 54 | 55 | The commands ignore the original `directory` argument and instead takes flags 56 | that are passed FZF. Run `fzf --help` to see what flags you can pass. A 57 | decent flag to use is `--tiebreak=index` which uses the initial order of the 58 | listed file as a secondary sort. `--tiebreak=end` will do a better job of 59 | sorting filename matches first. 60 | 61 | 62 | ## Options 63 | 64 | - `g:fzf_filemru_bufwrite` - Update the MRU on `BufWritePost`. This can be 65 | useful if you want your most saved files to appear near the top of the 66 | results. Default: `0` 67 | - `g:fzf_filemru_git_ls` - Use `git ls-tree` to display repo files before other 68 | files that are found with the the finder command. Always enabled for 69 | `ProjectMru`. Default: `0` 70 | - `g:fzf_filemru_ignore_submodule` - Ignore git submodule directories. Always 71 | enabled for `ProjectMru`. Default: `0` 72 | - `g:fzf_filemru_colors` - Colors for file prefixes. Uses the xterm 256 73 | [color palette][colors]. Default: `{'mru': 6, 'git': 3}`. 74 | 75 | **Note:** Even if git submodule files are ignored, they can still appear in the 76 | MRU. 77 | 78 | 79 | ## Command Line 80 | 81 | You can use `bin/filemru.sh` directly from the command line. It will act as 82 | [fzf](https://github.com/junegunn/fzf) for finding files, but will update the 83 | MRU with your file selections. This is currently not useful in the shell on 84 | its own. 85 | 86 | 87 | ### Command Line Switches 88 | 89 | - `--exclude` - Exclude a file from MRU output. Must be relative to the 90 | current directory. 91 | - `--files` | Just find files with MRU files displayed first and exit. 92 | - `--update` | Updates the MRU with the files specified after this switch. The 93 | files must be relative to the current directory. 94 | - `--git` | Use `git ls-tree` to display repo files after MRU files, but before 95 | other found files. 96 | - `--ignore-submodules` | Ignore git submodule directories. 97 | - `--mru-color` | Color for the MRU prefix. 98 | - `--git-color` | Color for the Git prefix. 99 | 100 | 101 | [colors]: https://upload.wikimedia.org/wikipedia/en/1/15/Xterm_256color_chart.svg 102 | -------------------------------------------------------------------------------- /bin/filemru.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | CACHE=${XDG_CACHE_HOME:-$HOME/.cache} 3 | MRU_MAX=200 4 | # MRU_FILE is a list of files that were selected in FZF through this script. 5 | # Each line is 3 comma separated values of: timestamp, select count, file name 6 | # The select count is used as a tie breaker for lines with the same timestamp. 7 | MRU_FILE=$CACHE/fzf_filemru 8 | DEFAULT_COMMAND="find . -path '*/\\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//" 9 | REAL_PWD="$(pwd -P)" 10 | 11 | if [ ! -d "$CACHE" ]; then 12 | mkdir -p "$CACHE" 13 | fi 14 | 15 | ignore_git_submodules=0 16 | git_ls=0 17 | print_files=0 18 | exclude_file="" 19 | color_mru="" 20 | color_git="" 21 | prefix_mru="mru" 22 | prefix_git="git" 23 | prefix_std=" - " 24 | 25 | update_mru() { 26 | # Update MRU_FILE with new selections. New selections are moved to the top 27 | # of the file. 28 | SELECTION_PATHS="" 29 | for fn in "${@}"; do 30 | fn="$PWD/$fn" 31 | if [ -e "$fn" ]; then 32 | SELECTION_PATHS+="$fn," 33 | fi 34 | done 35 | SELECTION_PATHS="${SELECTION_PATHS%,}" 36 | 37 | # Get the current contents. cat'ing the file in the command below blanks the 38 | # file before awk can get to it. I suspect witchcraft. 39 | CURRENT=$(cat $MRU_FILE) 40 | 41 | echo "$CURRENT" | awk -F',' ' 42 | BEGIN { 43 | ts = '$(date +%s)' 44 | ts -= (ts % 120) 45 | lastfound = 1 46 | split("'$SELECTION_PATHS'", files, ",") 47 | for (i in files) { 48 | selections[files[i]] = 1 49 | } 50 | } 51 | 52 | ($3 in selections) { found[NR] = 1; lastfound++ } { lines[NR] = $0 } 53 | 54 | END { 55 | if (lastfound > 1) { 56 | for (line in found) { 57 | $0 = lines[line]; $1 = ts; $2 += 1; print $1","$2","$3 58 | } 59 | } 60 | else { 61 | for (sel in selections) { 62 | print ts",1,"sel; 63 | } 64 | } 65 | 66 | i = 1 67 | p = 1 68 | while (i <= NR && p <= '$MRU_MAX') { 69 | if (!(i in found)) { 70 | print lines[i]; 71 | p++; 72 | } 73 | i++; 74 | } 75 | }' | sort -t, -k1 -k2 -g -r > $MRU_FILE 76 | } 77 | 78 | 79 | while [[ $# > 0 ]]; do 80 | case $1 in 81 | --exclude) 82 | exclude_file="$PWD/$2" 83 | shift 84 | ;; 85 | --files) 86 | print_files=1 87 | ;; 88 | --git) 89 | git_ls=1 90 | ;; 91 | --ignore-submodules) 92 | ignore_git_submodules=1 93 | ;; 94 | --update) 95 | shift 96 | update_mru $@ 97 | exit $? 98 | ;; 99 | --mru-color) 100 | color_mru=$2 101 | shift 102 | ;; 103 | --git-color) 104 | color_git=$2 105 | shift 106 | ;; 107 | esac 108 | shift 109 | done 110 | 111 | MRU=$(mktemp -t "fzf-filesmru.XXXXXXXX") 112 | GREP_EXCLUDE=$(mktemp -t "fzf-filesmru.ignore.XXXXXXXX") 113 | GREP_EXCLUDE_CMD="grep -v -x -F -f $GREP_EXCLUDE" 114 | 115 | cleanup() { 116 | rm -f "$MRU" 117 | rm -f "$GREP_EXCLUDE" 118 | } 119 | 120 | trap cleanup EXIT 121 | 122 | 123 | git_root=$(git rev-parse --show-toplevel 2> /dev/null) 124 | if [[ $ignore_git_submodules -eq 1 && -n "$git_root" && -e "$git_root/.gitmodules" ]]; then 125 | for p in $(git submodule foreach -q 'git ls-tree --name-only --full-name -r HEAD | awk "\$0=\"$path/\"\$0"' 2>/dev/null); do 126 | p="$git_root/$p" 127 | echo "${p##$REAL_PWD/}" >> $GREP_EXCLUDE 128 | done 129 | fi 130 | 131 | 132 | if [ -f "$MRU_FILE" ]; then 133 | files=$(cat "$MRU_FILE" | cut -d, -f3 | grep "^$PWD/") 134 | for fn in $files; do 135 | if [ -e "$fn" ]; then 136 | cut_fn="${fn##$PWD/}" 137 | echo "$cut_fn" >> $GREP_EXCLUDE 138 | if [ "$fn" != "$exclude_file" ]; then 139 | if [ -n "$color_mru" ]; then 140 | printf "\e[38;5;${color_mru}m${prefix_mru}\e[m ${cut_fn}\n" >> $MRU 141 | else 142 | echo "${prefix_mru} $cut_fn" >> $MRU 143 | fi 144 | fi 145 | fi 146 | done 147 | fi 148 | 149 | 150 | if [[ -n "$git_root" && $git_ls -eq 1 ]]; then 151 | for p in $(git ls-tree --name-only -r HEAD 2> /dev/null | $GREP_EXCLUDE_CMD); do 152 | p="$git_root/$p" 153 | [[ ! -e "$p" ]] && continue 154 | cut_fn="${p##$REAL_PWD/}" 155 | echo "$cut_fn" >> $GREP_EXCLUDE 156 | if [ "$p" != "$exclude_file" ]; then 157 | if [ -n "$color_git" ]; then 158 | printf "\e[38;5;${color_git}m${prefix_git}\e[m ${cut_fn}\n" >> $MRU 159 | else 160 | echo "${prefix_git} $cut_fn" >> $MRU 161 | fi 162 | fi 163 | done 164 | fi 165 | 166 | FIND_CMD=${FZF_DEFAULT_COMMAND:-$DEFAULT_COMMAND} 167 | if [ -s "$GREP_EXCLUDE" ]; then 168 | FIND_CMD+=" | $GREP_EXCLUDE_CMD" 169 | fi 170 | FIND_CMD+=" | awk '\$0=\"${prefix_std} \"\$0'" 171 | 172 | 173 | # Just find files and exit 174 | if [ $print_files -eq 1 ]; then 175 | cat $MRU 176 | sh -c "$FIND_CMD" 177 | exit $? 178 | fi 179 | 180 | 181 | # Act as FZF and update FILE_MRU after selection is made 182 | FIND_CMD="cat ""\$_FZF_MRU"" && $FIND_CMD" 183 | SELECTIONS=($(exec env _FZF_MRU="$MRU" FZF_DEFAULT_COMMAND="$FIND_CMD" fzf --ansi --nth=2 | awk '{ print $2 }')) 184 | 185 | if [ ${#SELECTIONS[@]} -eq 0 ]; then 186 | exit $? 187 | fi 188 | 189 | update_mru "${SELECTIONS[@]}" 190 | echo "${SELECTIONS[@]}" 191 | -------------------------------------------------------------------------------- /plugin/fzf-filemru.vim: -------------------------------------------------------------------------------- 1 | if exists('g:fzf_filemru_loaded') | finish | endif 2 | let g:fzf_filemru_loaded = 1 3 | 4 | let s:filemru_bin = resolve(printf('%s/../bin/filemru.sh', expand(':p:h'))) 5 | let s:ignore_patterns = '\.git/\|\_^/tmp/' 6 | 7 | 8 | function! s:update_mru(files) abort 9 | let selections = filter(copy(a:files), '!empty(v:val) && v:val !~# s:ignore_patterns') 10 | if empty(selections) 11 | return 12 | endif 13 | 14 | let l:cmd = [s:filemru_bin, '--update'] 15 | if has('nvim') 16 | " Use Neovim's jobstart to avoid the delay 17 | call jobstart(l:cmd + selections) 18 | else 19 | call system(join(l:cmd + map(selections, 'shellescape(v:val)'), ' ')) 20 | endif 21 | endfunction 22 | 23 | 24 | function! s:cmd_update_mru(verbose, ...) abort 25 | let cwd = getcwd() 26 | let arg_files = filter(copy(a:000), '!empty(v:val)') 27 | 28 | if empty(arg_files) && empty(&buftype) && &buflisted 29 | let arg_files = filter([expand('%')], '!empty(v:val)') 30 | endif 31 | 32 | let update_files = [] 33 | for fname in arg_files 34 | let fname = fnamemodify(fname, ':p') 35 | let prefix = strpart(fname, 0, strlen(cwd)) 36 | if prefix !=# cwd 37 | if a:verbose 38 | echohl ErrorMsg 39 | echo 'Not in current dirctory:' fname 40 | echohl None 41 | endif 42 | continue 43 | endif 44 | 45 | call add(update_files, strpart(fname, strlen(prefix) + 1)) 46 | endfor 47 | 48 | if !empty(update_files) 49 | call s:update_mru(update_files) 50 | elseif a:verbose 51 | echohl ErrorMsg 52 | echo 'No files to add to MRU' 53 | echohl None 54 | endif 55 | endfunction 56 | 57 | 58 | " Create FZF options. Wraps the 'sink*' to clean the file list and update the 59 | " MRU before passing it to the common_sink. 60 | " Reference: 61 | " https://github.com/junegunn/fzf.vim/blob/5a088b24269352885d80525258040bfda4685b1c/autoload/fzf/vim.vim#L404-L415 62 | function s:wrap_options(options) abort 63 | try 64 | let wrapped = fzf#wrap('', a:options) 65 | catch /E117/ 66 | let wrapped = fzf#vim#wrap(a:options) 67 | echohl WarningMsg 68 | echomsg '[fzf-filemru] junegunn/fzf is outdated.' 69 | echohl None 70 | endtry 71 | 72 | let wrapped.common_sink = remove(wrapped, 'sink*') 73 | function! wrapped.sink(lines) abort 74 | let selections = [] 75 | for l in a:lines 76 | let l = substitute(l, '^\(\s*\(git\|mru\|\-\)\s\+\)', '', '') 77 | call add(selections, l) 78 | endfor 79 | call s:update_mru(selections) 80 | return self.common_sink(selections) 81 | endfunction 82 | let wrapped['sink*'] = remove(wrapped, 'sink') 83 | return wrapped 84 | endfunction 85 | 86 | 87 | function! s:invoke(git_ls, ignore_submodule, options) abort 88 | let fzf_source = s:filemru_bin 89 | let exclude = expand('%') 90 | if empty(&l:buftype) && !empty(exclude) 91 | let fzf_source .= ' --exclude '.exclude 92 | endif 93 | 94 | if a:git_ls 95 | let fzf_source .= ' --git' 96 | endif 97 | 98 | if a:ignore_submodule 99 | let fzf_source .= ' --ignore-submodules' 100 | endif 101 | 102 | let colors = get(g:, 'fzf_filemru_colors', {'mru': 6, 'git': 3}) 103 | for c in keys(colors) 104 | let cn = get(colors, c, '') 105 | if !cn 106 | continue 107 | endif 108 | let fzf_source .= printf(' --%s-color %d', c, cn) 109 | endfor 110 | 111 | let fzf_source .= ' --files' 112 | 113 | call fzf#vim#files('', s:wrap_options({ 114 | \ 'source': fzf_source, 115 | \ 'options': a:options.' --ansi --nth=2', 116 | \ })) 117 | endfunction 118 | 119 | 120 | function! s:fzf_filemru(...) abort 121 | call s:invoke( 122 | \ get(g:, 'fzf_filemru_git_ls', 0), 123 | \ get(g:, 'fzf_filemru_ignore_submodule', 0), 124 | \ join(a:000, ' ')) 125 | endfunction 126 | 127 | 128 | function! s:fzf_projectmru(...) abort 129 | call s:invoke(1, 1, join(a:000, ' ')) 130 | endfunction 131 | 132 | 133 | command! -nargs=* FilesMru call s:fzf_filemru() 134 | command! -nargs=* ProjectMru call s:fzf_projectmru() 135 | command! -nargs=* -bang UpdateMru call s:cmd_update_mru(0, ) 136 | 137 | 138 | if get(g:, 'fzf_filemru_bufwrite', 0) 139 | augroup fzf_filemru 140 | autocmd! 141 | autocmd BufWritePost * call s:update_mru([expand('%')]) 142 | augroup END 143 | endif 144 | --------------------------------------------------------------------------------