A vim plugin to manage projects and sessions.
Features
- Switch between projects
Project-wide
-
Search files by name
-
Find in files
-
Find and replace
-
Run tasks
-
Search git log and file history
-
Config
-
Session (optional)
You can save session per branch if both vim feature
joband shell commandtailare present.
Add / Open a project
Search files / Find in files / Find and replace in a project
- Basic Usage
- Installation
- Uninstallation
- Workflow
- Commands
- Config and Keymappings
- Global variables
- Statusline
- Debug
- Credits
:Project <path>:ProjectNew <path>:ProjectList:ProjectSearchFiles:ProjectFindInFiles:ProjectRun:ProjectGitLog:ProjectFileHistory
In a list, filter by typing. Select an item by c-j, c-k or the arrow keys. Then press Enter to open it.
See Config and Keymappings for details.
It's recommended to install fd and one of rg or ag to improve search performance.
You can install this plugin just like others.
How to install
-
Plugin 'leafOfTree/vim-project' -
cd ~/.vim/bundle git clone https://github.com/leafOfTree/vim-project --depth 1 -
Plug 'leafOfTree/vim-project' -
Or manually, clone this plugin to
path/to/this_plugin, and add it tortpin vimrcset rtp+=path/to/this_plugin
Important
If some commands run much slower in vim than its equivalent in terminal, try resetting shell to sh or cmd.exe.
To enable file icon in front of file name, you need to install
- Nerd Fonts choose one and use it in Vim
- nerdfont.vim
- glyph-palette.vim (Optional, color icons)
You need to remove this plugin as well as config_home (default: ~/.vim/vim-project-config).
-
Add
:Project <path>|:ProjectNew <path> -
Open
:ProjectList|:ProjectOpen <name> -
Quit
:ProjectQuit| Open another project | Quit vim -
Remove
:ProjectRemove <name>
| Command | Description |
|---|---|
Project <path> |
Add an existing folder at path as project, then open it |
ProjectNew <path> |
Create a new folder as project at path by running predefined tasks |
| ProjectList | Show all projects |
| ProjectSearchFiles | Search files by name |
| ProjectSearchFilesRest | Reset search files, useful when new files added outside |
| ProjectFindInFiles | Find given string/regexp (at least 2 chars) in files |
| ProjectRun | Run a task defined by tasks config |
| ProjectGitLog | Show git log |
| ProjectGitFileHistory | Show file history |
| ProjectGitBranch | Show all git branches |
| ProjectGitTag | Show all git tags |
| ProjectRoot | Open project root |
| ProjectConfig | Open project config init.vim (effective after save) |
| ProjectAllConfig | Open all projects config project.add.vim |
| ProjectInfo | Show project brief info |
| ProjectAllInfo | Show project all info |
ProjectOpen <name> |
Open a project by name |
ProjectRemove <name> |
Remove a project by name |
ProjectRename <name> <new_name> |
Rename a project of name to new_name. Its folder's name is renamed, too |
| ProjectQuit | Quit project |
ProjectIgnore <path> |
Ignore project for auto detection |
You can try adjusting
wildmenu,wildmodefor enhanced command-line completion
path: If path is relative or a project name , it'll search with both current working directory and g:vim_project_config.project_base as path base . In addition, you can use Tab to auto complete the path.
Relative means path doesn't start with
/,~orC:/.
Optional: you can add "note" which will be shown on project list by :Project <path> "note for project".
Example
Project /path/to/demo
Project /path/to/demo "This is a demo"
" Current working directory (:pwd) is /path/to
Project demo
Project ../to
" g:vim_project_config.project_base is set to ['/path/to']
Project demo-
pathcan be a relative path or a name. you can use Tab to auto complete. -
If
pathis a relative path, it's based onnew_project_base(if not empty) or current working directory -
If
pathis a git url,ProjectNewwill usegit clone <url>to create a new project.
new_tasks defines tasks to create new project.
name: task namecmd: command to runargs: optional, the value is from what user types and will be appended tocmd
By default, the config is
\'new_tasks': [
\{ 'name': 'git', 'cmd': 'git clone', 'args': 'url' },
\{ 'name': 'empty', 'cmd': 'mkdir' },
\],
\`new_project_base`: ''new_tasks_post_cmd defines the command to run after project is created. By default, it's empty ''.
Example
let g:vim_project_config = {
\...
\'new_tasks': [
\ { 'name': 'mdbook', 'cmd': 'mdbook init' },
\ { 'name': 'vite', 'cmd': 'npm create vite@latest' },
\ { 'name': 'create react app', 'cmd': 'npx create-react-app' },
\ { 'name': 'spring', 'cmd': 'spring init' },
\ { 'name': 'degit', 'cmd': 'degit', 'args': 'url' },
\ { 'name': 'git', 'cmd': 'git clone', 'args': 'url' },
\ { 'name': 'empty directory', 'cmd': 'mkdir' },
\],
\'new_tasks_post_cmd': 'touch README.md && git init && git add . && git commit -m "Init"',
\`new_project_base`: '/path/to/projects',
\...new_tasks_post_cmd can also be a Function reference (Funcref). Its arguments are the project name (string), the task (dict), and the actual cmd (string).
function! NewTasksPostCmd(name, task, cmd)
if a:task.name == 'git'
return
endif
return 'touch README.md && git init && git add . && git commit -m "Init"'
\.'git remote add origin https://github.com/username/'.a:name
endfunction
...
\'new_tasks_post_cmd': function('NewTasksPostCmd'),-
:ProjectSearchFilesUnder the hood, it tries [fd,find,glob (vim function)] for the first available one as the search engine. -
:ProjectFindInFilesUnder the hood, it tries [rg, ag,grep,vimgrep (vim function)] for the first available one as the search engine.
It's recommended to install one of [fd, find] and one of [rg, ag, grep]. They have better performance especially for Windows users and large projects. However, none of them will respect .gitignore serving as a search engine for consistency.
To enable :ProjectFindInFiles on both normal and visually selected word, use noremap as follows
noremap <silent> <c-f> :ProjectFindInFiles<cr>For consistency, the behaviors are controlled as below no matter which engine is used.
Both Search files and Find in files
-
Include & Exclude
Check the following options in the config.
includeexclude
The following are specific options to extend the above. The final result is like
include (global + local)+search_include (global + local).search_includefind_in_files_includesearch_excludefind_in_files_exclude
you can use glob patterns like
dist,*.png, or**/dist.
Find in files
-
Match case
Prefix your input with
\C. By default, it's case insensitive. -
Regexp
Prefix your input with
\E. By default, it's treated as a literal/fixed string.
Find and replace
Please note this feature is not fully tested. It may cause unexpected changes to your files. Always remember to commit your changes before running it. Feel free to open an issue if anything goes wrong.
When Find in files, you can press
- c-r to start to replace
- Enter or c-y to confirm
- c-d to dismiss any item on the list
:ProjectRun run a task defined by tasks. A task contains
name: task namecmd: command to run in terminal through shellargs: optional, the value is from what user types and will be appended tocmdcd: optional, change directory to current project relativelyshow,hide: optional, both are a list of patterns to fiter task output
For example, with below local config
let g:vim_project_local_config = {
\'tasks': [
\{
\'name': 'npm',
\'cmd' : 'npm run',
\'args': 'script name',
\'cd' : 'webapp',
\},
\{
\'name': 'build',
\'cmd': 'npm build'
\'show': ['tests run.*in', '^\s\+'],
\'hide': ['info'],
\},
\{
\'name': 'terminal',
\'cmd': ''
\},
\],
\}Run, Rerun, Stop: You can press
- Enter to run/rerun the task
- c-q to stop the task and remove its output.
- c-o to open the corresponding terminal buffer
Vim terminal:
If you set cmd to empty string '', it'll call :terminal to open a new Vim terminal window.
:ProjectGitLog: Show git log:ProjectGitFileHistory: Show history of current opened file. Also Works on visually selected lines.:ProjectGitBranch: Show branches:ProjectGitTag: Show tags
Important
If some commands run much slower in vim than its equivalent in terminal, try resetting shell to sh or cmd.exe.
:ProjectGitStatus: Show local changes. You can create changelist to organize your local changes. Support basic operations likecommit,rollback,push, andpull.
Below are git related key mappings for the corresponding diff, changes, and local changes buffers.
let g:vim_project_config.git_diff_mappings = {
\'jump_to_source': "\<cr>",
\}
let g:vim_project_config.git_changes_mappings = {
\'open_file': "\<cr>",
\}
let g:vim_project_config.git_local_changes_mappings = {
\'commit': 'c',
\'rollback_file': 'R',
\'open_changelist_or_file': "\<cr>",
\'new_changelist': 'a',
\'move_to_changelist': 'm',
\'rename_changelist': 'r',
\'delete_changelist': 'd',
\'pull': 'u',
\'push': 'p',
\'pull_and_push': 'P',
\}The config consists of following two parts
g:vim_project_config(global, in.vimrc)g:vim_project_local_config(project local, in project'sinit.vim)
The g:vim_project_config should be set in .vimrc. Its default value is below. You can copy it as a starting point.
let g:vim_project_config = {
\'config_home': '~/.vim/vim-project-config',
\'project_base': ['~'],
\'use_session': 0,
\'use_viminfo': 0,
\'open_root_when_use_session': 0,
\'check_branch_when_use_session': 0,
\'project_root': './',
\'auto_load_on_start': 0,
\'include': ['./'],
\'exclude': ['.git', 'node_modules', '.DS_Store', '.github', '.next'],
\'search_include': [],
\'find_in_files_include': [],
\'search_exclude': [],
\'find_in_files_exclude': [],
\'auto_detect': 'no',
\'auto_detect_file': ['.git', '.svn'],
\'ask_create_directory': 'no',
\'project_views': [],
\'file_mappings': {},
\'tasks': [],
\'new_tasks': [
\{ 'name': 'git', 'cmd': 'git clone', 'args': 'url' },
\{ 'name': 'empty', 'cmd': 'mkdir' },
\],
\'new_project_base': '',
\'new_tasks_post_cmd': '',
\'commit_message': '',
\'debug': 0,
\}
" Keymappings for list prompt
let g:vim_project_config.list_mappings = {
\'open': "\<cr>",
\'close_list': "\<esc>",
\'clear_char': ["\<bs>", "\<c-a>"],
\'clear_word': "\<c-w>",
\'clear_all': "\<c-u>",
\'prev_item': ["\<c-k>", "\<up>"],
\'next_item': ["\<c-j>", "\<down>"],
\'first_item': ["\<c-h>", "\<left>"],
\'last_item': ["\<c-l>", "\<right>"],
\'scroll_up': "\<c-p>",
\'scroll_down': "\<c-n>",
\'paste': "\<c-b>",
\'switch_to_list': "\<c-o>",
\}
let g:vim_project_config.list_mappings_projects = {
\'prev_view': "\<s-tab>",
\'next_view': "\<tab>",
\}
let g:vim_project_config.list_mappings_search_files = {
\'open_split': "\<c-s>",
\'open_vsplit': "\<c-v>",
\'open_tabedit': "\<c-t>",
\}
let g:vim_project_config.list_mappings_find_in_files = {
\'open_split': "\<c-s>",
\'open_vsplit': "\<c-v>",
\'open_tabedit': "\<c-t>",
\'replace_prompt': "\<c-r>",
\'replace_dismiss_item': "\<c-d>",
\'replace_confirm': "\<cr>",
\}
let g:vim_project_config.list_mappings_run_tasks = {
\'run_task': "\<cr>",
\'stop_task': "\<c-q>",
\'open_task_terminal': "\<c-o>",
\}
" Checkout revision on git log and file history list.
" You can go back to branch head by :ProjectGitBranch
let g:vim_project_config.list_mappings_git = {
\'checkout_revision': "\<c-o>",
\}
let g:vim_project_config.list_mappings_git_branch = {
\'merge_branch': "\<c-o>",
\}
| Option | Description |
|---|---|
| config_home | The directory for all config files |
| project_base | A list of base directories used for path in :Project path |
| use_session | Save and load project-local session |
| use_viminfo | Save and load project-local viminfo (or shada for neovim) |
| open_root_when_use_session | When session used, always open project root at the beginning |
| check_branch_when_use_session | When session used, keep one for each branch |
| project_root | Starting directory or file when fist launching project |
| auto_detect | Auto detect projects when opening a file. Choose 'always', 'ask', or 'no' |
| auto_detect_file | File used to detect potential projects |
| auto_load_on_start | Auto load a project if Vim starts within its directory |
| ask_create_directory | Ask if need to create directory when :Project <name> doesn't find it |
| list_mappings | Keymappings for list prompt |
| include | Including folders |
| exclude | Excluding folders/files |
| search_include | Including folders for search files |
| find_in_files_include | Including folders for find in files |
| search_exclude | Excluding folders/files for search files |
| find_in_files_exclude | Excluding folders/files for find in files |
| file_mappings | Keymappings to switch between files quickly |
| tasks | Tasks to run using vim 'terminal' feature |
| new_project_base | The base directory used for :ProjectNew path. |
| project_views | Define project views by [[show-pattern, hide-pattern?], ...] |
| commit_message | Default commit message. Can be string or Function reference. |
| debug | Show debug messages |
The config files are located at ~/.vim/vim-project-config/, where ~/.vim/vim-project-config/<project-name>/ is for each project.
:ProjectAllConfig and :ProjectConfig will open corresponding config files.
~/.vim/vim-project-config/
| project.add.vim " Added projects
| project.igonre.vim " Ignored projects
| <project-name>/
| init.vim
| quit.vim
| sessions/ " session for each branch when use_session is '1'
| master.vim
| dev.vim
| viminfo/ " viminfo file when use_viminfo is '1'
| main.shada " for neovim
| .viminfo " for vim
Open
- Load project viminfo (when enabled)
- Load project session (when enabled)
- Source project
init.vim
Quit
- Save project viminfo (when enabled)
- Save project session (when enabled)
- Source project
quit.vim
init.vim will get automatically reloaded after you change it.
These options, list or dict types are extended, others overridden, via g:vim_project_local_config in the project's init.vim. E.g.
let g:vim_project_local_config = {
\'include': ['./'],
\'exclude': ['.git', 'node_modules', '.DS_Store', '.github', '.next'],
\'project_root': './',
\'use_session': 0,
\'open_root_when_use_session': 0,
\'check_branch_when_use_session': 0,
\}For the project local file_mappings, see below.
You can define mappings to frequently changed files in the project's init.vim.
For example as below, you can
- Switch to file
autoload/project.vimby'aand so on - Switch between files
['autoload/project.vim', 'plugin/project.vim']by'l - Switch between file types such as
*.htmland*.cssat the same path with't - Switch to file returned by user-defined function with
'c - Switch to file returned by
:h lambdaexpression with'd
function! StyleInUpperDir()
let upper_dir = expand('%:p:h:h')
let name = expand('%:r')
return upper_dir.'/'.name.'.css'
endfunction
let g:vim_project_local_config.file_mappings = {
\'r': 'README.md',
\'a': 'autoload/project.vim',
\'p': 'plugin/project.vim',
\'l': ['autoload/project.vim', 'plugin/project.vim'],
\'t': ['html', 'css'],
\'c': function('StyleInUpperDir'),
\'i': {-> $vim_project_config.'/init.vim'},
\'d': {-> expand('%:p:h:h').'/'.expand('%:r').'.css'},
\}With file_open_types, you can use 'a, 'va, 'sa, 'ta' to edit file in different ways
let g:vim_project_config.file_open_types = {
\'': 'edit',
\'v': 'vsplit',
\'s': 'split',
\'t': 'tabedit',
\}Viminfo can be used to keep registers, marks, v:oldfiles, etc.
To load and save project-local viminfo, you can add
let g:vim_project_config = {
\...
\'use_viminfo': 1,
\...
\}You may disable global viminfo to make each project more isolated
if has('nvim')
set shada=""
else
set viminfo=""
endifTo load and save project-local session, you can add
let g:vim_project_config = {
\...
\'use_session': 1,
\...
\}See :h sessionoptions for what to save and restore.
In project list, you can switch between different views with tab and s-tab. You can set project_views to [[show_pattern, hide_patten?], ...] like
let g:vim_project_config.project_views = [
\['vim', 'plugin'],
\['^vue'],
\['^react$'],
\['.*', 'archived']
\]g:vim_projectdict: current project info$vim_projectstring: current project path$vim_project_configstring: current project config path
You can get current project info from g:vim_project. Try echo g:vim_project after opening a project.
For example, define a function called GetProjectInfo and add %{GetProjectInfo()} to statusline to show current project name and branch.
function! GetProjectInfo()
if exists('g:vim_project') && !empty(g:vim_project)
let name = g:vim_project.name
let branch = g:vim_project.branch
if empty(branch)
return '['.name.']'
else
return '['.name.', '.branch.']'
endif
else
return ''
endif
endfunction
set statusline=%<%t%m\ %y\ %=%{GetProjectInfo()}Or you can show project info on window title
set title titlestring=%{GetTitle()}
function! GetTitle()
if exists('g:vim_project') && !empty(g:vim_project)
return g:vim_project.name.' - '.expand('%')
else
return expand('%:p').' - '.expand('%')
endif
endfunctionIf something goes wrong, you can try
:ProjectInfo:ProjectAllInfo:echo g:vim_project:let g:vim_project_config.debug = 1
Thanks to timsofteng for the great idea. It all started with that.






