├── .vimrc ├── Dockerfile ├── LICENSE ├── README.md ├── autoload ├── fzfproject.vim └── fzfproject │ ├── autoroot.vim │ └── find.vim ├── doc └── images │ └── fzf-project.gif ├── plugin └── fzf-switch-project.vim └── tests └── test.vadar /.vimrc: -------------------------------------------------------------------------------- 1 | call plug#begin('~/.vim/plugged') 2 | 3 | Plug 'junegunn/fzf.vim' 4 | Plug 'junegunn/fzf' 5 | Plug 'tpope/vim-fugitive' 6 | Plug 'benwainwright/fzf-project' 7 | Plug 'junegunn/vader.vim' 8 | 9 | call plug#end() 10 | 11 | let g:fzfSwitchProjectWorkspaces = [ '~/repos' ] 12 | 13 | let g:fzfSwitchProjectProjects = [ '~/single-folder' ] 14 | 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | RUN apt-get update && apt-get install -y fzf git vim curl 3 | RUN curl -fLo ~/.vim/autoload/plug.vim --create-dirs \ 4 | https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim 5 | RUN mkdir -p ~/plugin 6 | COPY . /root/plugin/ 7 | COPY .vimrc /root/ 8 | RUN mkdir -p ~/single-folder 9 | RUN mkdir -p ~/repos/repo1 10 | RUN mkdir -p ~/repos/repo2 11 | RUN mkdir -p ~/repos/repo3 12 | RUN git config --global user.email "bwainwright28@gmail.com" 13 | RUN git config --global user.name "Ben Wainwright" 14 | RUN cd ~/single-folder && git init && touch foo && git add foo && git commit -m "initial commit" 15 | RUN cd ~/repos/repo1 && git init && touch foo && git add foo && git commit -m "initial commit" 16 | RUN cd ~/repos/repo2 && git init && touch foo && git add foo && git commit -m "initial commit" 17 | RUN cd ~/repos/repo3 && git init && touch foo && git add foo && git commit -m "initial commit" 18 | RUN vim +PlugInstall +qall 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ben Wainwright 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fzf-project 2 | 3 | This plugin provides an easy way of switching between project directories 4 | indexed from a specified workspace folder or folders. It's based on an idea I 5 | originally stole from [kieran-ohara](https://github.com/kieran-ohara)'s 6 | dotfiles. 7 | 8 | ![Screen Capture](./doc/images/fzf-project.gif) 9 | 10 | ## Install 11 | 12 | #### Install via Plug 13 | 14 | ```vim 15 | Plug 'junegunn/fzf.vim' "requirement from benwainwright/fzf-project 16 | Plug 'tpope/vim-fugitive' "requirement from benwainwright/fzf-project 17 | Plug 'benwainwright/fzf-project' 18 | ``` 19 | 20 | #### Install via Vundle 21 | 22 | ```vim 23 | Plugin 'junegunn/fzf.vim' "requirement from benwainwright/fzf-project 24 | Plugin 'tpope/vim-fugitive' "requirement from benwainwright/fzf-project 25 | Plugin 'benwainwright/fzf-project' 26 | ``` 27 | 28 | ## Usage 29 | 30 | Running `FzfSwitchProject` in command mode will produce a list of folders from 31 | within your workspace folders. When you select a project, the working directory 32 | is changed and you are presented with a fzf list of files to switch to. 33 | 34 | ## AutoRooting 35 | 36 | Installing `FzfSwitchProject` will automatically `cd` to the root directory of 37 | any given project when you open a file. It does this by recursively locating the 38 | nearest `.git` folder in the directory hierarchy. For this reason, is 39 | recommended to use `git` with this plugin 40 | 41 | ## Configure 42 | 43 | ```vim 44 | let g:fzfSwitchProjectWorkspaces = [ '~/workspace1', '~/workspace2' ] 45 | ``` 46 | 47 | Automatically list projects in the above folders 48 | 49 | ```vim 50 | let g:fzfSwitchProjectProjects = [ '~/folder1', '~/folder2' ] 51 | ``` 52 | 53 | Add individual folders to the project list (I use it for my 54 | dotfiles folder) 55 | 56 | 57 | ```vim 58 | let g:fzfSwitchProjectProjectDepth = 1 " default 59 | ``` 60 | 61 | Set the project folder depth. When this is set to 1 (the default), only the _immediate children_ of workspace folders are considered project folders. If it is set to a number greater than 1, then the project finder will recurse that many times to find project folders. E.g. if it is set to 2, the grand children of workspace folders are considered project folders, and if set to 3, the great-grandchildren. 62 | 63 | Note, that if during recursion any folder is found to contain a ".git" folder (and is therefore a git project), that folder is automatically considered a project folder and recursion will not continue. 64 | 65 | If this setting is set to the string 'infinite' (or indeed any value other than a number greater than 0), recursion continues infinitely until exhaustion or a `.git` project is found. 66 | 67 | ```vim 68 | let g:fzfSwitchProjectFindFilesCommand = 'git ls-files --others --exclude-standard --cached' " default 69 | ``` 70 | 71 | Command that is executed to get a list of all the files in a given project 72 | 73 | ```vim 74 | let g:fzfSwitchProjectAlwaysChooseFile = 0 75 | ``` 76 | 77 | Don't automatically open a file picker once project is selected 78 | 79 | ```vim 80 | let g:fzfSwitchProjectCloseOpenedBuffers = 0 81 | ``` 82 | 83 | Set this to 1 if you want to close and delete all opened buffers after switching to a different project, it will only close and delete opened buffers if all updated buffers are saved. 84 | 85 | ## Commands 86 | 87 | - `FzfSwitchProject` - open project switcher 88 | - `FzfChooseProjectFile` - switch file within project 89 | 90 | ## Dependencies 91 | 92 | - [fzf.vim](https://github.com/junegunn/fzf.vim) 93 | - [fugitive](https://github.com/tpope/vim-fugitive) 94 | -------------------------------------------------------------------------------- /autoload/fzfproject.vim: -------------------------------------------------------------------------------- 1 | let s:workspaces = get(g:, 'fzfSwitchProjectWorkspaces', []) 2 | let s:projects = get(g:, 'fzfSwitchProjectProjects', []) 3 | let s:chooseFile = get(g:, 'fzfSwitchProjectAlwaysChooseFile', 1) 4 | let s:projectDepth = get(g:, 'fzfSwitchProjectProjectDepth', 1) 5 | let s:closeOpenedBuffers = get(g:, 'fzfSwitchProjectCloseOpenedBuffers', 0) 6 | let s:debug = get(g:, 'fzfSwitchProjectDebug', 0) 7 | 8 | let s:commands = {} 9 | 10 | function! s:getAllDirsFromWorkspaces(workspaces, depth) 11 | if len(a:workspaces) == 0 12 | return [] 13 | endif 14 | 15 | let l:dirs = globpath(join(a:workspaces, ','), '*/', 1) 16 | 17 | let l:projectFolders = [] 18 | let l:nonProjectFolders = [] 19 | 20 | for dir in split(l:dirs, "\n") 21 | if has('win32') || has('win64') 22 | let dir = substitute(dir, "\\", "\/", "g") 23 | endif 24 | if FugitiveIsGitDir(dir . '/.git') || a:depth == s:projectDepth 25 | call add(l:projectFolders, fnamemodify(dir, ':h')) 26 | else 27 | call add(l:nonProjectFolders, fnamemodify(dir, ':h')) 28 | endif 29 | endfor 30 | 31 | return l:projectFolders + s:getAllDirsFromWorkspaces(l:nonProjectFolders, a:depth + 1) 32 | endfunction 33 | 34 | let s:projectsFromWorkspaces = s:getAllDirsFromWorkspaces(s:workspaces, 1) 35 | let s:projectList = s:projectsFromWorkspaces + s:projects 36 | 37 | function! fzfproject#execute(command, dir, context) 38 | let l:command = a:command . ' ' . a:dir 39 | if s:debug ==# 1 40 | echom("FZFProject (" . a:context . ") executing command: '" . l:command . "'") 41 | endif 42 | execute l:command 43 | endfunction 44 | 45 | function! fzfproject#changeDir(dir, context) 46 | call fzfproject#execute('cd', fnameescape(a:dir), a:context) 47 | endfunction 48 | 49 | function! s:setFileToSwitchTo(lines) 50 | if(len(a:lines) > 0) 51 | let l:query = a:lines[1] 52 | 53 | let l:commandMap = { 54 | \ 'ctrl-x': 'split', 55 | \ 'ctrl-v': 'vertical split', 56 | \ 'ctrl-t': 'tabe' 57 | \ } 58 | 59 | let s:editCommand = get(l:commandMap, l:query, 'edit') 60 | let s:setFileToSwitchTo = a:lines[2] 61 | endif 62 | endfunction 63 | 64 | function! fzfproject#switch() 65 | let l:is_win = has('win32') || has('win64') 66 | let l:opts = { 67 | \ 'dir': l:is_win ? $TEMP : '/tmp', 68 | \ 'sink': function('s:switchToProjectDir'), 69 | \ 'source': s:formatProjectList(s:projectList), 70 | \ 'down': '40%' 71 | \ } 72 | let l:wrapped = fzf#wrap(l:opts) 73 | call fzf#run(l:opts) 74 | endfunction 75 | 76 | function! s:switchToProjectDir(projectLine) 77 | try 78 | let autochdir = &autochdir 79 | set noautochdir 80 | let l:parts = matchlist(a:projectLine, '\(\S\+\)\s\+\(\S\+\)') 81 | let l:path = s:commands[a:projectLine]['path'] 82 | let w:fzfProjectPath = l:path 83 | 84 | if s:closeOpenedBuffers 85 | execute 'bufdo bwipeout' 86 | endif 87 | 88 | call fzfproject#changeDir(l:path, "projectSwitcher") 89 | 90 | if s:chooseFile 91 | call fzfproject#find#file(0, l:path, s:commands[a:projectLine]['command']) 92 | " Fixes issue with NeoVim 93 | " See https://github.com/junegunn/fzf/issues/426#issuecomment-158115912 94 | if has("nvim") && !has("nvim-0.5.0") 95 | call feedkeys('i') 96 | endif 97 | endif 98 | 99 | finally 100 | let &autochdir = autochdir 101 | endtry 102 | endfunction 103 | 104 | 105 | function! fzfproject#finalProjectList() 106 | return s:projectList 107 | endfunction 108 | 109 | function! s:formatProjectList(dirs) 110 | let l:dirParts = [ ] 111 | let l:longest = 0 112 | for dir in a:dirs 113 | if type(dir) == type({}) 114 | let l:name = has_key(dir, "name") ? dir['name'] : fnamemodify(dir['path'], ':t') 115 | let l:command = dir['command'] 116 | let l:dirPath = dir['path'] 117 | else 118 | let l:name = fnamemodify(dir, ':t') 119 | let l:command = '' 120 | let l:dirPath = dir 121 | endif 122 | 123 | let l:length = len(l:name) 124 | if l:length > l:longest 125 | let l:longest = l:length 126 | endif 127 | let dir = { 'name' : l:name, 'dir' : fnamemodify(l:dirPath, ':h'), 'command': l:command, 'fullPath': l:dirPath } 128 | call add(l:dirParts, dir) 129 | endfor 130 | return s:generateProjectListLines(l:dirParts, l:longest) 131 | endfunction 132 | 133 | function! s:generateProjectListLines(dirParts, longest) 134 | let l:outputLines = [ ] 135 | for dir in a:dirParts 136 | let l:padding = a:longest - len(dir['name']) 137 | let l:line = dir['name'] 138 | \ . repeat(' ', l:padding) 139 | \ . ' ' . dir['dir'] 140 | 141 | let s:commands[l:line] = { 'command': dir['command'], 'path': dir['fullPath'] } 142 | 143 | call add(l:outputLines, l:line) 144 | endfor 145 | return l:outputLines 146 | endfunction 147 | -------------------------------------------------------------------------------- /autoload/fzfproject/autoroot.vim: -------------------------------------------------------------------------------- 1 | let s:workspaces = get(g:, 'fzfSwitchProjectWorkspaces', []) 2 | 3 | function! fzfproject#autoroot#switchroot() 4 | if getbufinfo('%')[0]['listed'] && filereadable(@%) 5 | call fzfproject#autoroot#doroot() 6 | endif 7 | endfunction 8 | 9 | let s:dirs = fzfproject#finalProjectList() 10 | 11 | function! fzfproject#autoroot#doroot(...) 12 | 13 | if a:0 > 0 14 | let l:rootToTry = a:1 15 | else 16 | let l:rootToTry = FugitiveGitDir() 17 | endif 18 | 19 | if index(s:dirs, l:rootToTry) == -1 20 | 21 | let l:newRoot = fnamemodify(l:rootToTry, ":h") 22 | 23 | if l:newRoot !=# l:rootToTry 24 | call fzfproject#autoroot#doroot(fnamemodify(l:rootToTry, ":h")) 25 | endif 26 | 27 | else 28 | call fzfproject#changeDir(l:rootToTry, "doRoot") 29 | endif 30 | endfunction 31 | -------------------------------------------------------------------------------- /autoload/fzfproject/find.vim: -------------------------------------------------------------------------------- 1 | let s:projects = get(g:, 'fzfSwitchProjectProjects', []) 2 | let s:listFilesCommand = get(g:, 'fzfSwitchProjectFindFilesCommand', 'git ls-files --others --exclude-standard --cached') 3 | let s:debug = get(g:, 'fzfSwitchProjectDebug', 0) 4 | 5 | function! s:switchToFile(dir, lines) 6 | 7 | if(len(a:lines) > 0) 8 | try 9 | let autochdir = &autochdir 10 | set noautochdir 11 | let l:query = a:lines[1] 12 | 13 | let l:commandMap = { 14 | \ 'ctrl-x': 'split', 15 | \ 'ctrl-v': 'vertical split', 16 | \ 'ctrl-t': 'tabe' 17 | \ } 18 | 19 | let l:editCommand = get(l:commandMap, a:lines[1], 'edit') 20 | if(len(a:lines) > 1) 21 | let l:file = a:lines[2] 22 | call fzfproject#execute(l:editCommand, fnameescape(a:dir .. "/" .. l:file), "switchToFile") 23 | else 24 | let s:yesNo = input("Create '" . l:query . "'? (y/n) ") 25 | if s:yesNo ==? 'y' || s:yesNo ==? 'yes' 26 | execute l:editCommand . ' ' . l:query 27 | write 28 | endif 29 | endif 30 | finally 31 | let &autochdir = autochdir 32 | endtry 33 | endif 34 | endfunction 35 | 36 | function! fzfproject#find#file(root_first, dir, command) 37 | 38 | if(a:root_first) 39 | call fzfproject#autoroot#doroot() 40 | endif 41 | 42 | let l:dir = a:dir == -1 ? getcwd() : a:dir 43 | 44 | echom l:dir 45 | 46 | let l:command = '' 47 | 48 | for project in s:projects 49 | if (type(project) == type({}) && expand(project['path']) == expand(l:dir)) 50 | let l:command = project['command'] 51 | endif 52 | endfor 53 | 54 | if (l:command == '') 55 | let l:command = a:command ==# '' ? s:listFilesCommand : a:command 56 | end 57 | 58 | if s:debug ==# 1 59 | echom("FZFProject list command: '" . l:command . "'") 60 | endif 61 | 62 | let l:is_win = has('win32') || has('win64') 63 | let l:opts = { 64 | \ 'dir': l:is_win ? $TEMP : '/tmp', 65 | \ 'sink*' : function('s:switchToFile', [l:dir]), 66 | \ 'down': '40%', 67 | \ 'options': [ 68 | \ '--print-query', 69 | \ '--expect=ctrl-t,ctrl-v,ctrl-x', 70 | \ '--header', 'Choose existing file, or enter the name of a new file', 71 | \ '--prompt', 'Filename> ' 72 | \ ] 73 | \ } 74 | 75 | let l:opts['source'] = 'cd ' . (l:is_win ? '/d' : '') .. l:dir .. ' && ' .. l:command . (l:is_win ? '' : ' | uniq') 76 | 77 | return fzf#run(fzf#wrap(l:opts)) 78 | endfunction 79 | 80 | -------------------------------------------------------------------------------- /doc/images/fzf-project.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benwainwright/fzf-project/4cfff21b56575b1e82d1a5c0f8714d718ce4088d/doc/images/fzf-project.gif -------------------------------------------------------------------------------- /plugin/fzf-switch-project.vim: -------------------------------------------------------------------------------- 1 | augroup projectSwitcher 2 | autocmd! 3 | autocmd BufEnter * call fzfproject#autoroot#switchroot() 4 | autocmd VimEnter * call fzfproject#autoroot#doroot() 5 | augroup END 6 | 7 | command! FzfSwitchProject call fzfproject#switch() 8 | command! FzfChooseProjectFile call fzfproject#find#file(1, -1, '') 9 | -------------------------------------------------------------------------------- /tests/test.vadar: -------------------------------------------------------------------------------- 1 | Do: 2 | FzfSwitchProject\repo2\:pwd\ 3 | 4 | Expect: 5 | ~/test 6 | 7 | 8 | 9 | --------------------------------------------------------------------------------