project.nvim 
https://github.com/user-attachments/assets/0e10c4e8-f930-47a0-9058-956622e8f547
project.nvim is a Neovim plugin written in Lua that automatically sets your cwd
to the current project root, and also allows you to manage, access and selectively include
your projects in a history file.
This plugin allows you to navigate through, rename, "bookmark" and/or discard your projects, according to your needs.
This was originally forked from ahmedkhalf/project.nvim. Ever since I decided to extend it and address issues.
You can check some sample videos in EXAMPLES.md.
Features
- Automatically sets the
cwdto the project root directory using pattern matching (LSP optionally) - Projects can be assigned a name (
:Project history rename [...]) - Users can define custom project roots, see Custom Projects
- Users can control whether to run this or not by filetype/buftype
- Functional
checkhealthhook (:checkhealth project) - Vim help documentation (
:h project-nvim) - Logging capabilities
- Natively supports
.nvim.luafiles vim.uimenu support- Supports
oil.nvimbuffers - Lualine Integration
- Telescope Integration
:Telescope projects fzf-luaIntegrationnvim-treeIntegrationneo-treeIntegrationmini.starterIntegrationpicker.nvimIntegrationsnacks.nvimIntegration
Table of Contents
Installation
Requirements:
- Neovim >=
v0.11 fd(REQUIRED FOR SESSION MANAGEMENT)ibhagwan/fzf-lua(OPTIONAL, RECOMMENDED)wsdjeg/picker.nvim(OPTIONAL, RECOMMENDED)
nvim-telescope/telescope.nvim(OPTIONAL, RECOMMENDED)
If you want to add instructions for your plugin manager of preference please raise a BLANK ISSUE.
Use any plugin manager of your choosing.
vim-plug
if has('nvim-0.11')
Plug 'DrKJeff16/project.nvim'
" OPTIONAL
Plug 'nvim-telescope/telescope.nvim' | Plug 'nvim-lua/plenary.nvim'
Plug 'wsdjeg/picker.nvim'
Plug 'ibhagwan/fzf-lua'
lua << EOF
require('project').setup()
EOF
endif
lazy.nvim
{
'DrKJeff16/project.nvim',
dependencies = { -- OPTIONAL. Choose any of the following
{
'nvim-telescope/telescope.nvim',
dependencies = { 'nvim-lua/plenary.nvim' },
},
'wsdjeg/picker.nvim',
'folke/snacks.nvim',
'ibhagwan/fzf-lua',
},
opts = {},
}
If you wish to lazy-load this plugin:
{
'DrKJeff16/project.nvim',
cmd = { 'Project' }, -- Lazy-load by commands
dependencies = { -- OPTIONAL. Choose any of the following
{ 'nvim-telescope/telescope.nvim', dependencies = { 'nvim-lua/plenary.nvim' } },
'wsdjeg/picker.nvim',
'folke/snacks.nvim',
'ibhagwan/fzf-lua',
},
opts = {},
}
pckr.nvim
require('pckr').add({
{
'DrKJeff16/project.nvim',
requires = { -- OPTIONAL. Choose any of the following
'nvim-lua/plenary.nvim',
'nvim-telescope/telescope.nvim',
'wsdjeg/picker.nvim',
'folke/snacks.nvim',
'ibhagwan/fzf-lua',
},
config = function()
require('project').setup()
end,
}
})
nvim-plug
require('plug').add({
{
'DrKJeff16/project.nvim',
depends = { -- OPTIONAL
'nvim-lua/plenary.nvim',
'nvim-telescope/telescope.nvim',
'wsdjeg/picker.nvim',
'folke/snacks.nvim',
'ibhagwan/fzf-lua',
},
config = function()
require('project').setup()
end,
},
})
paq-nvim
local paq = require('paq')
paq({
'DrKJeff16/project.nvim',
-- OPTIONAL. Choose any of the following
'nvim-lua/plenary.nvim',
'nvim-telescope/telescope.nvim',
'wsdjeg/picker.nvim',
'folke/snacks.nvim',
'ibhagwan/fzf-lua',
})
vim.pack
vim.pack.add({
'https://github.com/DrKJeff16/project.nvim',
})
LuaRocks
The package can be found in the LuaRocks webpage.
luarocks install project.nvim # Global install
luarocks install --local project.nvim # Local install
Configuration
To enable the plugin you must call setup():
require('project').setup()
Click here to view the setup defaults
You can find these in config/defaults.lua.
By default, setup() loads with the following options:
{
before_attach = nil, ---@type nil|fun(target_dir: string, method: string, bufnr?: integer)
on_attach = nil, ---@type nil|fun(target_dir: string, method: string, bufnr?: integer)
lsp = {
enabled = true,
ignore = {}, -- LSP clients to ignore
-- If `true`, no pattern matching will be used as a backup.
-- WARNING: ENABLE AT YOUR OWN DISCRETION!!!!
no_fallback = false,
-- Whether to double-check the LSP root with the pattern matching method.
use_pattern_matching = false,
},
custom_projects = {}, -- Read the `Custom Projects` section below
manual_mode = false,
-- This list is permanent, and any new entries are appended. You can leave this empty
patterns = {
'.git',
'.github',
'_darcs',
'.hg',
'.bzr',
'.svn',
'Pipfile',
'pyproject.toml',
'.pre-commit-config.yaml',
'.pre-commit-config.yml',
'.csproj',
'.sln',
'.nvim.lua',
'.neoconf.json',
'neoconf.json',
},
different_owners = {
allow = false, -- Allow adding projects with a different owner to the project session
notify = true, -- Notify the user when a project with a different owner is found
},
enable_autochdir = false, -- Set this to `true` if you prefer having `vim.o.autochdir` set to `true`
show_by_name = false, -- Show projects by their name instead of their full path
show_hidden = false, -- Show hidden files (global)
exclude_dirs = {}, -- Add any directory to exclude (absolute path). Keep in mind that this is recursive
silent_chdir = true,
scope_chdir = 'global', ---@type 'global'|'tab'|'win'
history = {
save_dir = vim.fn.stdpath('data'),
save_file = 'project_history.json',
size = 100, -- The maximum number of history entries to write in your history file
},
log = {
enabled = false,
max_size = 1.1, -- This size is in Mebibytes (MiB), A.K.A. 1MiB -> 1024KiB
logpath = vim.fn.stdpath('state'),
},
snacks = {
enabled = false,
opts = {
-- path_icons = {},
-- icon = {},
hidden = false, -- Show hidden files
layout = 'select',
sort = 'newest', ---@type 'newest'|'oldest'
title = 'Select Project',
-- Show project entries either by their path (`'paths'`) or their custom name (`'names'`)
show = 'paths', ---@type 'paths'|'names'
},
},
fzf_lua = {
enabled = false,
sort = 'newest', ---@type 'newest'|'oldest'
-- Show project entries either by their path (`'paths'`) or their custom name (`'names'`)
show = 'paths', ---@type 'paths'|'names'
},
picker = {
enabled = false,
hidden = false, -- Show hidden files
sort = 'newest', ---@type 'newest'|'oldest'
-- Show project entries either by their path (`'paths'`) or their custom name (`'names'`)
show = 'paths', ---@type 'paths'|'names'
},
disable_on = {
-- This list is permanent, and any new entries are appended. You can leave this empty
ft = {
'',
'NvimTree',
'TelescopePrompt',
'TelescopeResults',
'alpha',
'checkhealth',
'lazy',
'log',
'ministarter',
'neo-tree',
'notify',
'nvim-pack',
'packer',
'qf',
},
-- This list is permanent, and any new entries are appended. You can leave this empty
bt = { 'help', 'nofile', 'nowrite', 'terminal' },
},
telescope = {
disable_file_picker = false,
mappings = {
n = {
R = 'rename_project',
b = 'browse_project_files',
d = 'delete_project',
f = 'find_project_files',
r = 'recent_project_files',
s = 'search_in_project_files',
w = 'change_working_directory',
},
i = {
['<C-b>'] = 'browse_project_files',
['<C-d>'] = 'delete_project',
['<C-f>'] = 'find_project_files',
['<C-n>'] = 'rename_project',
['<C-r>'] = 'recent_project_files',
['<C-s>'] = 'search_in_project_files',
['<C-w>'] = 'change_working_directory',
},
},
prefer_file_browser = false,
sort = 'newest', ---@type 'oldest'|'newest'
tilde = false, -- If `true`, project paths like `'/home/foo/...'` will become `'~/...'`
},
}
Custom Projects
You can pre-define custom project roots in your setup. This is particularly useful for directories
that don't have .git or any other patterns in them.
[!CAUTION] The
pathfield has to be an absolute path!
For example:
local expand = require('project.util').strip_slash -- WRAPPER FOR `vim.fn.fnamemodify()`, EXPANDS PATHS
require('project').setup({
custom_projects = {
{ path = expand('~/Projects') }, -- The `name` field is optional
{ path = expand('~/Documents'), name = 'Documents' },
},
})
Pattern Matching
project.nvim comes with a vim-rooter-inspired pattern matching expression engine
to give you better handling of your projects.
For your convenience here come some examples:
-
To specify the root is a certain directory, prefix it with
=:patterns = { '=src' } -
To specify the root has a certain directory or file (which may be a glob), just add it to the pattern list:
patterns = { '.git', '.github', '*.sln', 'build/env.sh' } -
To specify the root has a certain directory as an ancestor (useful for excluding directories), prefix it with
^:patterns = { '^fixtures' } -
To specify the root has a certain directory as its direct ancestor/parent (useful when you put working projects in a common directory), prefix it with
\>:patterns = { '>Latex' } -
To exclude a pattern, prefix it with `!`
patterns = { '!.git/worktrees', '!=extras', '!^fixtures', '!build/env.sh' }
[!IMPORTANT] Make sure to put your pattern exclusions first, and then the patterns you DO want included.
Also if you have
allow_patterns_for_lspenabled, it will also work somewhat for your LSP clients.
Lualine
You can add the project.nvim component to your statusline using lualine.nvim:
lualine_b = {
{
"project",
-- Can be:
-- - `'short'` - Only shows the basename of the project root directory
-- - `'full'` - Shows the full path but without expanding the home directory
-- - `'full_expanded'` - Shows the full, expanded path
-- - `'name'` - (default) Will show the current project's name. ONLY WORKS IF HISTORY
-- HAS BEEN MIGRATED, OTHERWISE `'short'` WILL BE USED
format = 'name',
-- Text to display when no project root is found (set to `nil` or empty string to disable)
no_project = 'N/A',
-- The separator
separator = " ",
-- Optional table of two strings set as enclosing characters.
-- Set to `nil` to disable it
--
-- e.g. `enclose_pair = { '(', ')' }` ==> `(<YOUR_PROJECT>)`
-- `enclose_pair = { '<', ']' }` ==> `<<YOUR_PROJECT>]`
-- `enclose_pair = { nil, 'a' }` ==> `<YOUR_PROJECT>a`
enclose_pair = nil,
}
}
Nvim Tree
Make sure these flags are enabled to support nvim-tree.lua:
require('nvim-tree').setup({
sync_root_with_cwd = true,
respect_buf_cwd = true,
update_focused_file = {
enable = true,
update_root = true,
},
})
Neo Tree
You can use :Neotree filesystem ... when changing a project:
vim.keymap.set('n', '<YOUR-TOGGLE-MAP>', ':Neotree filesystem toggle reveal_force_cwd<CR>', opts)
vim.keymap.set('n', '<YOUR-SHOW-MAP>', ':Neotree filesystem show reveal_force_cwd<CR>', opts)
vim.keymap.set('n', '<YOUR-FLOAT-MAP>', ':Neotree filesystem float reveal_force_cwd<CR>', opts)
-- ... and so on
Telescope
To enable telescope.nvim integration use the following
code in your config:
require('telescope').setup()
require('telescope').load_extension('projects')
After that you can now call it from the command line:
:Telescope projects
You can also configure the picker when calling require('telescope').setup(). CREDITS: @ldfwbebp
require('telescope').setup({
extensions = {
projects = {
prompt_prefix = " ",
layout_strategy = "horizontal",
layout_config = {
anchor = "N",
height = 0.25,
width = 0.6,
prompt_position = "bottom",
},
},
},
})
Telescope Mappings
project.nvim comes with the following mappings for Telescope:
| Normal Mode | Insert Mode | Action |
|---|---|---|
b |
<C-b> |
browse_project_files |
d |
<C-d> |
delete_project |
f |
<C-f> |
find_project_files |
n |
<C-n> |
rename_project |
r |
<C-r> |
recent_project_files |
s |
<C-s> |
search_in_project_files |
w |
<C-w> |
change_working_directory |
_You can find the Actions in telescope/_extensions/projects/actions.lua_.
mini.starter
If you use nvim-mini/mini.starter you can include the following snippet in your MiniStarter setup:
require('mini.starter').setup({
evaluate_single = true,
items = {
{ name = 'Projects', action = 'Project', section = 'Projects' }, -- Runs `:Project`
{ name = 'Recent Projects', action = 'ProjectRecents', section = 'Projects' }, -- `:Project recents`
-- Other items...
},
})
picker.nvim
This plugin has a custom integration with @wsdjeg's picker.nvim.
If enabled, the :Project picker command will be available to you.
To enable it you'll need the plugin installed, then in your setup:
require('project').setup({
picker = {
enabled = true,
sort = 'newest', -- 'newest' or 'oldest'
hidden = false, -- Show hidden files
}
})
Mappings:
| Normal Mode | Description |
|---|---|
<C-d> |
Delete the selected project |
<C-r> |
Rename the selected project |
<C-w> |
Changes the cwd to the selected project |
You can find the integration in:
snacks.nvim
This plugin has a custom integration with snacks.nvim.
If enabled, the :Project snacks command will be available to you.
require('project.extensions.snacks').pick()
To enable and configure it you'll need the plugin installed, then in your setup:
require('project').setup({
snacks = {
enabled = true, -- Will enable the `:Project snacks` command
opts = {
sort = 'newest',
hidden = false,
title = 'Select Project',
layout = 'select',
-- icon = {},
-- path_icons = {},
},
},
})
Mappings:
| Normal Mode | Description |
|---|---|
<C-d> |
Delete the selected project |
<C-r> |
Rename the selected project |
<C-w> |
Changes the cwd to the selected project |
You can find the integration in extensions/snacks.lua.
Usage
You may use the :Project user command in your cmdline to manage your experience.
Without any arguments being passed, a UI selection window with all the important operations will be opened.
See commands.lua for more info.
Extra Operations
You also have the following subcommands:
:Project[!] add [/path/to/project [/path/to/project [...]]]:
If no other args are passed, you'll be prompted to input any directory to be saved
to your project history (adding a ! will set the prompt to your cwd).
If one or more args are passed, you will not be prompted (the arguments must be valid paths).
:Project[!] config:
Will toggle a floating window with your current config.
To exit the window you can either press q in normal mode or by runnning :Project config again.
You can also print the output instead by running :Project! config.
:Project[!] delete [/path/to/project [...]]:
If no arguments are given, a popup with a list of your current projects will be opened.
If one or more arguments are passed, it will expect directories separated by a space.
The arguments have to be directories that are returned by Core.get_recent_projects()
and can be relative, absolute or un-expanded (~/path/to/project).
The command will attemptto parse the args and, unless a ! is passed (:Project! delete).
In that case, invalid args will be ignored.
If there's a successful deletion, you'll recieve a notification denoting success.
:Project[!] export [/path/to/file[.json] [<INT>]\]
[!WARNING] Use this subcommand with caution, as you may overwrite your files if doing something reckless!
Allows you to save your project history in a portable JSON format, and with a custom indent level if desired. If the target file already exists and is not empty then a confirmation prompt will appear.
Example usage:
:Project export " Will open a prompt
:Project export a " The output file will be `a.json`
:Project! export b 12 " The output file will be `b`, with a tab size of 12
:Project fzf-lua
[!IMPORTANT] This command works ONLY if you have
fzf-luainstalled and loaded andfzf_lua.enabledset totrue.
If the fzf-lua integration is enabled in your setup, it will open a fzf-lua picker for project.nvim.
Mappings:
| Mapping | Description |
|---|---|
<C-d> |
Delete the selected project |
<C-r> |
Rename the selected project |
See extensions/fzf-lua.lua
for more info.
:Project health
Simply runs :checkhealth project.
:Project[!] history [clear|rename [/path/to/project [/path/to/project] [...]\]\]
If the command is called without any extra arguments it'll toggle the project.nvim history file
in a new tab, which can be exited by pressing q in Normal Mode.
If you wish to rename a project you can call :Project history rename, with or without
any arguments passed to it.
If no arguments are passed, a custom UI list will open, showing you your projects to be renamed.
If one or more arguments are passed, these must be the paths to your projects (absolute or relative).
[!CAUTION] If called with the
clearargument (:Project[!] history clear) your project history will be cleared. If you want to avoid a "Yes/No" prompt you can call the command with a bang (!) to force it.
Usage:
:Project history
:Project[!] history clear
:Project history rename [/path/to/project [...]] " (NEEDS MIGRATION) Will rename the specified projects
:Project[!] import [/path/to/file[.json]\]
Allows you to retrieve any previously exported project history (through :Project export).
Example usage:
:Project import " Will open a prompt
:Project export a " Will be treated as `a.json`
:Project import a " Will be treated as `a.json`
:Project! export b " Will be treated as `b`
:Project! import b " Will be treated as `b`
:Project log [clear|close|open|toggle]
[!IMPORTANT] This command will not be available unless you set
log.enabled = truein your setup.
Handles the project.nvim log file. The valid arguments are:
:Project log " Toggles the window
:Project log clear " Clears the current log file. Will close any opened log window
:Project log close " Closes the Log Window
:Project log open " Opens the Log Window
:Project log toggle " Toggles the Log Window
:Project[!] picker
[!IMPORTANT] This command works ONLY if you have
picker.nviminstalled andpicker.enabledset totrue.
Runs project.nvim through picker.nvim.
If a bang is passed (:Project! picker) and you don't already have picker.hidden set to true,
then a selected project will show hidden files.
:Project recents
Will open a UI window showing you the list of your recently used projects.
:Project[!] root
[!NOTE] Useful if you have set
manual_modetotruein your setup.
A manual hook to set the working directory to the current file's root, using the available detection methods set by the user.
If a bang is passed (:Project! root) and your cwd is already in the project history, then
the command will not fail and will execute as if your cwd is not in the history already.
If successful, the command will execute the following code:
:lua require('project.core').on_buf_enter()
:Project[!] session
[!IMPORTANT] This command requires
fdto be installed for it to work!
Will open a custom picker with a selection of your current session projects (stored in History.session_projects).
If you select a session project, your cwd will be changed to what you selected.
If a bang is passed (:Project! session), then the UI will close if it's open. Otherwise,
another custom UI picker will appear for you to select the files/dirs.
Selecting a directory will open another UI picker with the project's contents, and so on.
:Project snacks
[!IMPORTANT] This command works ONLY if you have
snacks.nviminstalled andsnacks.enabledset totrue.
A dynamically enabled user command that runs project.nvim through snacks.nvim.
This is an alias for:
require('project.extensions.snacks').pick()
:Project telescope
[!IMPORTANT] This command works ONLY if you have
telescope.nviminstalled and loaded
A dynamicly enabled user command that runs the Telescope projects picker.
This is a shortcut for :Telescope projects.
API
The API can be found in core.lua.
get_project_root()
get_project_root() is an API utility for finding out about the current project's root, if any:
---@type string|nil, string|nil
local root, lsp_or_method = require('project').get_project_root()
get_recent_projects()
You can get a list of recent projects by running the code below:
local recent_projects = require('project').get_recent_projects() ---@type string[]
vim.notify(vim.inspect(recent_projects))
Where get_recent_projects() returns either an empty table {}
or a string array { '/path/to/project1', ... }.
get_config()
If setup() has been called, it returns a table containing the currently set options.
Otherwise it will return nil.
local config = require('project').get_config()
-- Using `vim.notify()`
vim.notify(vim.inspect(config))
-- Using `vim.print()`
vim.print(config)
get_history_paths()
If no valid args are passed to this function, it will return the following dictionary:
local get_history_paths = require('project').get_history_paths
-- A dictionary table containing all return values below
vim.print(get_history_paths())
--- { datapath = <datapath>, projectpath = <projectpath>, historyfile = <historyfile> }
Otherwise, if either 'datapath', 'projectpath' or 'historyfile' are passed,
it will return the string value of said arg:
-- The directory where `project` sets its `datapath`
vim.print(get_history_paths('datapath'))
-- The directory where `project` saves the project history
vim.print(get_history_paths('projectpath'))
-- The path to where `project` saves its recent projects history
vim.print(get_history_paths('historyfile'))
Utils
A set of utilities that get repeated across the board. You can import them as shown below:
local ProjUtil = require('project.util')
These utilities are in part inspired by my own utilities found in my Neovim config,
Jnvim.
See util.lua for further reference.
Troubleshooting
History File Not Created
If you're in a UNIX environment, make sure you have read, write and access permissions
(rwx) for the projectpath directory.
[!IMPORTANT] The default value is
vim.fn.stdpath('data')/project_nvim.json. See:h stdpath()for more info.
You can get the value of projectpath by running the following in the cmdline:
:lua vim.print(require('project').get_history_paths('projectpath'))
If you lack the required permissions for that directory, you can either:
- Delete that directory (RECOMMENDED)
- Run
chmod 755 <project/path>(755==>rwxr-xr-xfor UNIX users)
Alternatives
Show these much love!
nvim-telescope/telescope-project.nvimfolke/snacks.nvimcoffebar/neovim-projectLintaoAmons/cd-project.nvimwsdjeg/rooter.nvim