A Neovim plugin that detects your project root, tracks recent projects, and persists per-project sessions. Combines ideas from ahmedkhalf/project.nvim (LSP / pattern root detection) and coffebar/neovim-project (session integration, neo-tree state persistence).
- Root detection via LSP workspace folders or parent-directory pattern walk
- Rich pattern DSL:
=name,^ancestor,>parent,!negation, or glob-as-file-marker - Per-project session save/restore through
neovim-session-manager - Neo-tree expanded-directory state persisted inside the session file
- History file with per-project metadata (detection method, last-opened time, session count)
- Telescope extension for switching / deleting / searching projects
- Neovim 0.9+
- nvim-lua/plenary.nvim
- Shatur/neovim-session-manager
- echasnovski/mini.icons (for
display.lua) - Optional: nvim-neo-tree/neo-tree.nvim — enables expanded-dir persistence
- Optional: nvim-telescope/telescope.nvim — enables the project picker
The plugin will opportunistically use a logger module on the runtimepath
(calling require("logger").init("project")), falling back to a
vim.notify-based shim when none is available.
With lazy.nvim:
{
"njhoffman/project.nvim",
dependencies = {
"nvim-lua/plenary.nvim",
"Shatur/neovim-session-manager",
"echasnovski/mini.icons",
},
config = function()
require("project").setup({
-- options
})
end,
}Defaults (see lua/project/config.lua for the full list):
require("project").setup({
manual_mode = false,
detection_methods = { "lsp", "pattern" },
patterns = { ".project", "courseleaf.cfg", ".git", "_darcs", ".hg", ".bzr", ".svn", "Makefile" },
ignore_lsp = {},
exclude_dirs = {},
show_hidden = false,
silent_chdir = true,
paths = {
history = vim.fn.stdpath("data") .. "/project/project_history",
sessions = vim.fn.stdpath("data") .. "/project/project_sessions",
},
last_session_project_on_startup = true,
last_session_other_on_startup = true,
})Entries in patterns are walked up from the current buffer's directory:
| Prefix | Meaning | Example |
|---|---|---|
| (none) | A file matching this glob exists | Makefile |
= |
Directory name equals | =src |
^ |
Directory is descendant of this name | ^projects |
> |
Directory is immediate child of this | >workspace |
! |
Negation (skip this dir, keep going) | !node_modules |
List entries can also be tables — all patterns in a table are checked at every level before ascending to the parent, instead of one-pattern-per-walk.
| Command | Description |
|---|---|
:ProjectWrite |
Flush in-memory session-project data to the history file |
:ProjectLoadRecent [N] |
Load the Nth most recent project (default: last one) |
local project = require("project")
project.setup(opts)
project.get_project_root() -- returns (dir, method_string)
project.get_recent_projects() -- returns (list, meta_table)require("telescope").load_extension("project")
-- pickers:
:Telescope project projects -- all projects grouped by workspace
:Telescope project recent -- recent only
:Telescope project nvim -- filtered to 'nvim' workspacemake format-check # stylua --check
make lint # selene
make test # plenary-busted headless
make check # all threeA single spec:
make test-file FILE=tests/project/globtopattern_spec.luaEach line in the history file must carry these fields:
path— exists as a directory at load timemethod— the detection method string (e.g.pattern Makefile,lsp)method_type— required whenmethod == "lsp"(the LSP client name)num— integer visit counterlast— positive Unix timestamp
On load, invalid lines are dropped with a vim.notify WARN describing why,
and the on-disk file is rewritten without them. On save, invalid entries are
likewise skipped with a warning so malformed data never lands in the file.
If you're moving off the author's personal-config version of this code, the
default paths.history / paths.sessions are under stdpath("data")/project/...
now (previously .../project_nvim/...). Either move the old files once or
point paths.history / paths.sessions at the old location via setup().
Because validation is now strict, existing history entries that pre-date the
num / last metadata fields will be dropped with a warning on the first
load under 0.3.0+.
The logger dependency is optional: if a logger module is on the
runtimepath the plugin will use it, otherwise it falls back to a
vim.notify-based shim (see lua/project/utils/logger.lua).
MIT. See LICENSE.