mirror of
https://github.com/NotAShelf/direnv.nvim.git
synced 2025-10-03 15:33:35 +00:00
Merge pull request #7 from NotAShelf/massive-refactor
direnv.nvim: complete plugin overhaul
This commit is contained in:
commit
8e8c2a1d6e
3 changed files with 677 additions and 125 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,3 +1,6 @@
|
||||||
|
# Testing Configuration
|
||||||
|
test.lua
|
||||||
|
|
||||||
# Compiled Lua sources
|
# Compiled Lua sources
|
||||||
luac.out
|
luac.out
|
||||||
|
|
||||||
|
|
186
README.md
186
README.md
|
@ -1,39 +1,191 @@
|
||||||
# direnv.nvim
|
# direnv.nvim
|
||||||
|
|
||||||
Dead simple Neovim plugin to add automatic Direnv loading, inspired by
|
Dead simple Neovim plugin to add automatic Direnv loading, inspired by
|
||||||
`direnv.vim` and written in Lua.
|
`direnv.vim` and written in Lua for better performance and maintainability.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- Seamless integration with direnv for managing project environment variables
|
||||||
|
- Automatic detection of `.envrc` files in your workspace
|
||||||
|
- Proper handling of allowed, pending, and denied states
|
||||||
|
- Built-in `.envrc` editor with file creation wizard
|
||||||
|
- Statusline component showing real-time direnv status
|
||||||
|
- Event hooks for integration with other plugins
|
||||||
|
- Comprehensive API for extending functionality
|
||||||
|
|
||||||
|
### 📓 TODO
|
||||||
|
|
||||||
|
There are things direnv.nvim can _not_ yet do. Mainly, we would like to
|
||||||
|
integrate Treesitter for **syntax highlighting** similar to direnv.vim.
|
||||||
|
Unfortunately there isn't a TS grammar for Direnv, but we can port syntax.vim
|
||||||
|
from direnv.vim.
|
||||||
|
|
||||||
|
Additionally, it might be worth adding an option to allow direnv on, e.g.,
|
||||||
|
VimEnter if the user has configured to do so.
|
||||||
|
|
||||||
## 📦 Installation
|
## 📦 Installation
|
||||||
|
|
||||||
Install `direnv.nvim` with your favorite plugin manager, or clone it manually.
|
Install `direnv.nvim` with your favorite plugin manager, or clone it manually.
|
||||||
You will need to call the setup function to load the plugin.
|
You will need to call the setup function to load the plugin.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Neovim 0.8.0 or higher
|
||||||
|
- [direnv](https://direnv.net/) installed and available in your PATH
|
||||||
|
|
||||||
|
### Using lazy.nvim
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{
|
||||||
|
"NotAShelf/direnv.nvim",
|
||||||
|
config = function()
|
||||||
|
require("direnv").setup({})
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## 🚀 Usage
|
## 🚀 Usage
|
||||||
|
|
||||||
direnv.nvim will automatically call `direnv allow` in your current directory if
|
direnv.nvim will manage your .envrc files in Neovim by providing commands to
|
||||||
`direnv` is available in your PATH, and you have auto-loading enabled.
|
allow, deny, reload and edit them. When auto-loading is enabled, the plugin will
|
||||||
|
automatically detect and prompt for allowing `.envrc` files in your current
|
||||||
|
directory.
|
||||||
|
|
||||||
## 🔧 Configuration
|
### Commands
|
||||||
|
|
||||||
|
- `:Direnv allow` - Allow the current directory's .envrc file
|
||||||
|
- `:Direnv deny` - Deny the current directory's .envrc file
|
||||||
|
- `:Direnv reload` - Reload direnv for the current directory
|
||||||
|
- `:Direnv edit` - Edit the `.envrc` file (creates one if it doesn't exist)
|
||||||
|
- `:Direnv status` - Show the current direnv status
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
You can pass your config table into the `setup()` function or `opts` if you use
|
You can pass your config table into the `setup()` function or `opts` if you use
|
||||||
`lazy.nvim`.
|
`lazy.nvim`.
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
- `bin` (optional, type: string): the path to the Direnv binary. May be an
|
|
||||||
absolute path, or just `direnv` if it's available in your PATH. - Default:
|
|
||||||
`direnv`
|
|
||||||
- `autoload_direnv` (optional, type: boolean): whether to call `direnv allow`
|
|
||||||
when you enter a directory that contains an `.envrc`. - Default: `false`
|
|
||||||
- `keybindings` (optional, type: table of strings): the table of keybindings to
|
|
||||||
use.
|
|
||||||
- Default:
|
|
||||||
`{allow = "<Leader>da", deny = "<Leader>dd", reload = "<Leader>dr"}`
|
|
||||||
|
|
||||||
#### Example:
|
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
require("direnv").setup({
|
require("direnv").setup({
|
||||||
autoload_direnv = true,
|
-- Path to the direnv executable
|
||||||
|
bin = "direnv",
|
||||||
|
|
||||||
|
-- Whether to automatically load direnv when entering a directory with .envrc
|
||||||
|
autoload_direnv = false,
|
||||||
|
|
||||||
|
-- Statusline integration
|
||||||
|
statusline = {
|
||||||
|
-- Enable statusline component
|
||||||
|
enabled = false,
|
||||||
|
-- Icon to display in statusline
|
||||||
|
icon = "",
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Keyboard mappings
|
||||||
|
keybindings = {
|
||||||
|
allow = "<Leader>da",
|
||||||
|
deny = "<Leader>dd",
|
||||||
|
reload = "<Leader>dr",
|
||||||
|
edit = "<Leader>de",
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Notification settings
|
||||||
|
notifications = {
|
||||||
|
-- Log level (vim.log.levels.INFO, ERROR, etc.)
|
||||||
|
level = vim.log.levels.INFO,
|
||||||
|
-- Don't show notifications during autoload
|
||||||
|
silent_autoload = true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Statusline Integration
|
||||||
|
|
||||||
|
You can add direnv status to your statusline by using the provided function:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- For lualine
|
||||||
|
require('lualine').setup({
|
||||||
|
sections = {
|
||||||
|
lualine_x = {
|
||||||
|
function()
|
||||||
|
return require('direnv').statusline()
|
||||||
|
end,
|
||||||
|
'encoding',
|
||||||
|
'fileformat',
|
||||||
|
'filetype',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
-- For a Neovim-native statusline without plugins
|
||||||
|
vim.o.statusline = '%{%v:lua.require("direnv").statusline()%} ...'
|
||||||
|
```
|
||||||
|
|
||||||
|
The statusline function will show:
|
||||||
|
|
||||||
|
- Nothing when disabled or no .envrc is found
|
||||||
|
- "active" when the .envrc is allowed
|
||||||
|
- "pending" when the .envrc needs approval
|
||||||
|
- "blocked" when the .envrc is explicitly denied
|
||||||
|
|
||||||
|
## 🔍 API Reference
|
||||||
|
|
||||||
|
**Public Functions**
|
||||||
|
|
||||||
|
- `direnv.setup(config)` - Initialize the plugin with optional configuration
|
||||||
|
- `direnv.allow_direnv()` - Allow the current directory's `.envrc` file
|
||||||
|
- `direnv.deny_direnv()` - Deny the current directory's `.envrc` file
|
||||||
|
- `direnv.check_direnv()` - Check and reload direnv for the current directory
|
||||||
|
- `direnv.edit_envrc()` - Edit the `.envrc` file
|
||||||
|
- `direnv.statusline()` - Get a string for statusline integration
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local direnv = require("direnv")
|
||||||
|
|
||||||
|
direnv.setup({
|
||||||
|
autoload_direnv = true,
|
||||||
|
statusline = {
|
||||||
|
enabled = true,
|
||||||
|
},
|
||||||
|
keybindings = {
|
||||||
|
allow = "<Leader>ea", -- Custom keybinding example
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
-- You can also call functions directly
|
||||||
|
vim.keymap.set('n', '<Leader>er', function()
|
||||||
|
direnv.check_direnv()
|
||||||
|
end, { desc = "Reload direnv" })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Events
|
||||||
|
|
||||||
|
The plugin triggers a User autocmd event that you can hook into:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
vim.api.nvim_create_autocmd("User", {
|
||||||
|
pattern = "DirenvLoaded",
|
||||||
|
callback = function()
|
||||||
|
-- Code to run after direnv environment is loaded
|
||||||
|
print("Direnv environment loaded!")
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🫂 Special Thanks
|
||||||
|
|
||||||
|
I extend my thanks to the awesome [Lychee](https://github.com/itslychee),
|
||||||
|
[mrshmllow](https://github.com/mrshmllow) and
|
||||||
|
[diniamo](https://github.com/diniamo) for their invaluable assistance in the
|
||||||
|
creation of this plugin. I would also like to thank
|
||||||
|
[direnv.vim](https://github.com/direnv/direnv.vim) maintainers for their initial
|
||||||
|
work.
|
||||||
|
|
||||||
|
## 📜 License
|
||||||
|
|
||||||
|
direnv.nvim is licensed under the [MPL v2.0](./LICENSE). Please see the license
|
||||||
|
file for more details.
|
||||||
|
|
613
lua/direnv.lua
613
lua/direnv.lua
|
@ -1,9 +1,40 @@
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
--- @class DirenvConfig
|
||||||
|
--- @field bin string Path to direnv executable
|
||||||
|
--- @field autoload_direnv boolean Automatically load direnv when opening files
|
||||||
|
--- @field statusline table Configuration for statusline integration
|
||||||
|
--- @field statusline.enabled boolean Enable statusline integration
|
||||||
|
--- @field statusline.icon string Icon to show in statusline
|
||||||
|
--- @field keybindings table Keybindings configuration
|
||||||
|
--- @field keybindings.allow string Keybinding to allow direnv
|
||||||
|
--- @field keybindings.deny string Keybinding to deny direnv
|
||||||
|
--- @field keybindings.reload string Keybinding to reload direnv
|
||||||
|
--- @field keybindings.edit string Keybinding to edit .envrc
|
||||||
|
--- @field notifications table Notification settings
|
||||||
|
--- @field notifications.level integer Log level for notifications
|
||||||
|
--- @field notifications.silent_autoload boolean Don't show notifications during autoload
|
||||||
|
|
||||||
|
local cache = {
|
||||||
|
status = nil,
|
||||||
|
path = nil,
|
||||||
|
last_check = 0,
|
||||||
|
ttl = 5000, -- milliseconds before cache invalidation, then we can think about naming things
|
||||||
|
}
|
||||||
|
|
||||||
|
local notification_queue = {}
|
||||||
|
|
||||||
|
--- Check if an executable is available in PATH
|
||||||
|
--- @param executable_name string Name of the executable
|
||||||
|
--- @return boolean is_available
|
||||||
local function check_executable(executable_name)
|
local function check_executable(executable_name)
|
||||||
if vim.fn.executable(executable_name) ~= 1 then
|
if vim.fn.executable(executable_name) ~= 1 then
|
||||||
vim.notify(
|
vim.notify(
|
||||||
"Executable '" .. executable_name .. "' not found",
|
"Executable '"
|
||||||
|
.. executable_name
|
||||||
|
.. "' not found. Please install "
|
||||||
|
.. executable_name
|
||||||
|
.. " first.",
|
||||||
vim.log.levels.ERROR
|
vim.log.levels.ERROR
|
||||||
)
|
)
|
||||||
return false
|
return false
|
||||||
|
@ -11,6 +42,25 @@ local function check_executable(executable_name)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Get current working directory safely
|
||||||
|
--- @return string|nil cwd Current working directory or nil on error
|
||||||
|
local function get_cwd()
|
||||||
|
local cwd_result, err = vim.uv.cwd()
|
||||||
|
if err then
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.notify(
|
||||||
|
"Failed to get current directory: " .. err,
|
||||||
|
vim.log.levels.ERROR
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return cwd_result
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Setup keymaps for the plugin
|
||||||
|
--- @param keymaps table List of keymap definitions
|
||||||
|
--- @param mode string|table Vim mode for the keymap
|
||||||
local function setup_keymaps(keymaps, mode)
|
local function setup_keymaps(keymaps, mode)
|
||||||
for _, map in ipairs(keymaps) do
|
for _, map in ipairs(keymaps) do
|
||||||
local options = vim.tbl_extend(
|
local options = vim.tbl_extend(
|
||||||
|
@ -22,158 +72,505 @@ local function setup_keymaps(keymaps, mode)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
M.setup = function(user_config)
|
--- Safe notify function that works in both sync and async contexts
|
||||||
local config = vim.tbl_deep_extend("force", {
|
--- @param msg string Message to display
|
||||||
bin = "direnv",
|
--- @param level? integer Log level
|
||||||
autoload_direnv = false,
|
--- @param opts? table Additional notification options
|
||||||
keybindings = {
|
local function notify(msg, level, opts)
|
||||||
allow = "<Leader>da",
|
-- Ensure level is an integer
|
||||||
deny = "<Leader>dd",
|
level = level
|
||||||
reload = "<Leader>dr",
|
or (M.config and M.config.notifications.level or vim.log.levels.INFO)
|
||||||
},
|
opts = opts or {}
|
||||||
}, user_config or {})
|
opts = vim.tbl_extend("force", { title = "direnv.nvim" }, opts)
|
||||||
|
|
||||||
if not check_executable(config.bin) then
|
if vim.in_fast_event() then
|
||||||
return
|
table.insert(notification_queue, {
|
||||||
end
|
msg = msg,
|
||||||
|
level = level,
|
||||||
vim.api.nvim_create_user_command("Direnv", function(opts)
|
opts = opts,
|
||||||
local cmds = {
|
|
||||||
["allow"] = M.allow_direnv,
|
|
||||||
["deny"] = M.deny_direnv,
|
|
||||||
["reload"] = M.check_direnv,
|
|
||||||
}
|
|
||||||
local cmd = cmds[string.lower(opts.fargs[1])]
|
|
||||||
if cmd then
|
|
||||||
cmd()
|
|
||||||
end
|
|
||||||
end, {
|
|
||||||
nargs = 1,
|
|
||||||
complete = function()
|
|
||||||
return { "allow", "deny", "reload" }
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
setup_keymaps({
|
|
||||||
{
|
|
||||||
config.keybindings.allow,
|
|
||||||
function()
|
|
||||||
M.allow_direnv()
|
|
||||||
end,
|
|
||||||
{ desc = "Allow direnv" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
config.keybindings.deny,
|
|
||||||
function()
|
|
||||||
M.deny_direnv()
|
|
||||||
end,
|
|
||||||
{ desc = "Deny direnv" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
config.keybindings.reload,
|
|
||||||
function()
|
|
||||||
M.check_direnv()
|
|
||||||
end,
|
|
||||||
{ desc = "Reload direnv" },
|
|
||||||
},
|
|
||||||
}, "n")
|
|
||||||
|
|
||||||
-- If user has enabled autoloading, and current directory has an .envrc
|
|
||||||
-- then load it. This has performance implications as it will check for
|
|
||||||
-- a filepath on each BufEnter event.
|
|
||||||
if config.autoload_direnv and vim.fn.glob("**/.envrc") ~= "" then
|
|
||||||
local group_id = vim.api.nvim_create_augroup("DirenvNvim", {})
|
|
||||||
|
|
||||||
vim.api.nvim_create_autocmd({ "DirChanged" }, {
|
|
||||||
pattern = "global",
|
|
||||||
group = group_id,
|
|
||||||
callback = function()
|
|
||||||
M.check_direnv()
|
|
||||||
end,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
vim.schedule(function()
|
||||||
|
while #notification_queue > 0 do
|
||||||
|
local item = table.remove(notification_queue, 1)
|
||||||
|
vim.notify(item.msg, item.level, item.opts)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
vim.notify(msg, level, opts)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
M.allow_direnv = function()
|
--- Process any pending notifications
|
||||||
print("Allowing direnv...")
|
local function process_notification_queue()
|
||||||
os.execute("direnv allow")
|
vim.schedule(function()
|
||||||
|
while #notification_queue > 0 do
|
||||||
|
local item = table.remove(notification_queue, 1)
|
||||||
|
vim.notify(item.msg, item.level, item.opts)
|
||||||
|
end
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
M.deny_direnv = function()
|
--- Get current direnv status via JSON API
|
||||||
print("Denying direnv...")
|
--- @param callback function Callback function to handle result
|
||||||
os.execute("direnv deny")
|
M._get_rc_status = function(callback)
|
||||||
end
|
local loop = vim.uv or vim.loop
|
||||||
|
local now = math.floor(loop.hrtime() / 1000000) -- ns -> ms
|
||||||
|
if cache.status ~= nil and (now - cache.last_check) < cache.ttl then
|
||||||
|
return callback(cache.status, cache.path)
|
||||||
|
end
|
||||||
|
|
||||||
|
local cwd = get_cwd()
|
||||||
|
if not cwd then
|
||||||
|
return callback(nil, nil)
|
||||||
|
end
|
||||||
|
|
||||||
M._get_rc_status = function(_on_exit)
|
|
||||||
local on_exit = function(obj)
|
local on_exit = function(obj)
|
||||||
local status = vim.json.decode(obj.stdout)
|
if obj.code ~= 0 then
|
||||||
|
vim.schedule(function()
|
||||||
|
notify(
|
||||||
|
"Failed to get direnv status: "
|
||||||
|
.. (obj.stderr or "unknown error"),
|
||||||
|
vim.log.levels.ERROR
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
return callback(nil, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, status = pcall(vim.json.decode, obj.stdout)
|
||||||
|
if not ok or not status or not status.state then
|
||||||
|
return callback(nil, nil)
|
||||||
|
end
|
||||||
|
|
||||||
if status.state.foundRC == nil then
|
if status.state.foundRC == nil then
|
||||||
return _on_exit(nil, nil)
|
cache.status = nil
|
||||||
|
cache.path = nil
|
||||||
|
cache.last_check = now
|
||||||
|
return callback(nil, nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
_on_exit(status.state.foundRC.allowed, status.state.foundRC.path)
|
cache.status = status.state.foundRC.allowed
|
||||||
end
|
cache.path = status.state.foundRC.path
|
||||||
|
cache.last_check = now
|
||||||
|
|
||||||
return vim.system(
|
callback(status.state.foundRC.allowed, status.state.foundRC.path)
|
||||||
{ "direnv", "status", "--json" },
|
|
||||||
{ text = true, cwd = vim.fn.getcwd(-1, -1) },
|
|
||||||
on_exit
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
M._init = function(path)
|
|
||||||
vim.schedule(function()
|
|
||||||
vim.notify("Reloading " .. path)
|
|
||||||
end)
|
|
||||||
|
|
||||||
local cwd = vim.fs.dirname(path)
|
|
||||||
|
|
||||||
local on_exit = function(obj)
|
|
||||||
vim.schedule(function()
|
|
||||||
vim.fn.execute(vim.fn.split(obj.stdout, "\n"))
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
vim.system(
|
vim.system(
|
||||||
{ "direnv", "export", "vim" },
|
{ M.config.bin, "status", "--json" },
|
||||||
{ text = true, cwd = cwd },
|
{ text = true, cwd = cwd },
|
||||||
on_exit
|
on_exit
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Initialize direnv for current directory
|
||||||
|
--- @param path string Path to .envrc file
|
||||||
|
M._init = function(path)
|
||||||
|
local cwd = vim.fs.dirname(path)
|
||||||
|
local silent = M.config.notifications.silent_autoload
|
||||||
|
and vim.b.direnv_autoload_triggered
|
||||||
|
|
||||||
|
if not silent then
|
||||||
|
vim.schedule(function()
|
||||||
|
notify("Loading environment from " .. path, vim.log.levels.INFO)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local on_exit = function(obj)
|
||||||
|
if obj.code ~= 0 then
|
||||||
|
vim.schedule(function()
|
||||||
|
notify(
|
||||||
|
"Failed to load direnv: " .. (obj.stderr or "unknown error"),
|
||||||
|
vim.log.levels.ERROR
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.schedule(function()
|
||||||
|
local env_commands = vim.split(obj.stdout, "\n")
|
||||||
|
if #env_commands > 0 then
|
||||||
|
for _, cmd in ipairs(env_commands) do
|
||||||
|
if cmd ~= "" then
|
||||||
|
pcall(function()
|
||||||
|
vim.cmd(cmd)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not silent then
|
||||||
|
notify(
|
||||||
|
"direnv environment loaded successfully",
|
||||||
|
vim.log.levels.INFO
|
||||||
|
)
|
||||||
|
end
|
||||||
|
vim.api.nvim_exec_autocmds(
|
||||||
|
"User",
|
||||||
|
{ pattern = "DirenvLoaded", modeline = false }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.system(
|
||||||
|
{ M.config.bin, "export", "vim" },
|
||||||
|
{ text = true, cwd = cwd },
|
||||||
|
on_exit
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Allow direnv for current directory
|
||||||
|
M.allow_direnv = function()
|
||||||
|
M._get_rc_status(function(_, path)
|
||||||
|
if not path then
|
||||||
|
vim.schedule(function()
|
||||||
|
notify(
|
||||||
|
"No .envrc file found in current directory",
|
||||||
|
vim.log.levels.WARN
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.schedule(function()
|
||||||
|
notify("Allowing direnv for " .. path, vim.log.levels.INFO)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Capture dir before the async call
|
||||||
|
local cwd = get_cwd()
|
||||||
|
if not cwd then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.system(
|
||||||
|
{ M.config.bin, "allow" },
|
||||||
|
{ text = true, cwd = cwd },
|
||||||
|
function(obj)
|
||||||
|
if obj.code ~= 0 then
|
||||||
|
vim.schedule(function()
|
||||||
|
notify(
|
||||||
|
"Failed to allow direnv: " .. (obj.stderr or ""),
|
||||||
|
vim.log.levels.ERROR
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Clear cache to ensure we get fresh data
|
||||||
|
-- and then load the environment
|
||||||
|
cache.status = nil
|
||||||
|
M.check_direnv()
|
||||||
|
|
||||||
|
vim.schedule(function()
|
||||||
|
notify("direnv allowed for " .. path, vim.log.levels.INFO)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Deny direnv for current directory
|
||||||
|
M.deny_direnv = function()
|
||||||
|
M._get_rc_status(function(_, path)
|
||||||
|
if not path then
|
||||||
|
vim.schedule(function()
|
||||||
|
notify(
|
||||||
|
"No .envrc file found in current directory",
|
||||||
|
vim.log.levels.WARN
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.schedule(function()
|
||||||
|
notify("Denying direnv for " .. path, vim.log.levels.INFO)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local cwd = get_cwd()
|
||||||
|
if not cwd then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.system(
|
||||||
|
{ M.config.bin, "deny" },
|
||||||
|
{ text = true, cwd = cwd },
|
||||||
|
function(obj)
|
||||||
|
if obj.code ~= 0 then
|
||||||
|
vim.schedule(function()
|
||||||
|
notify(
|
||||||
|
"Failed to deny direnv: " .. (obj.stderr or ""),
|
||||||
|
vim.log.levels.ERROR
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
cache.status = nil
|
||||||
|
|
||||||
|
vim.schedule(function()
|
||||||
|
notify("direnv denied for " .. path, vim.log.levels.INFO)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Edit the .envrc file
|
||||||
|
M.edit_envrc = function()
|
||||||
|
M._get_rc_status(function(_, path)
|
||||||
|
if not path then
|
||||||
|
-- TODO: envrc can be in a different directory, e.g., the parent.
|
||||||
|
-- We should search for it backwards eventually.
|
||||||
|
local cwd = get_cwd()
|
||||||
|
if not cwd then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local envrc_path = cwd .. "/.envrc"
|
||||||
|
vim.schedule(function()
|
||||||
|
local create_new = vim.fn.confirm(
|
||||||
|
"No .envrc file found. Create one?",
|
||||||
|
"&Yes\n&No",
|
||||||
|
1
|
||||||
|
)
|
||||||
|
|
||||||
|
if create_new == 1 then
|
||||||
|
vim.cmd("edit " .. envrc_path)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd("edit " .. path)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Check and load direnv if applicable
|
||||||
M.check_direnv = function()
|
M.check_direnv = function()
|
||||||
local on_exit = function(status, path)
|
local on_exit = function(status, path)
|
||||||
if status == nil or path == nil then
|
if status == nil or path == nil then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Allowed
|
-- Status 0 means the .envrc file is allowed
|
||||||
if status == 0 then
|
if status == 0 then
|
||||||
return M._init(path)
|
return M._init(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Blocked
|
-- Status 2 means the .envrc file is explicitly blocked
|
||||||
if status == 2 then
|
if status == 2 then
|
||||||
|
vim.schedule(function()
|
||||||
|
notify(
|
||||||
|
path .. " is explicitly blocked by direnv",
|
||||||
|
vim.log.levels.WARN
|
||||||
|
)
|
||||||
|
end)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Status 1 means the .envrc file needs approval
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
local choice =
|
local choice = vim.fn.confirm(
|
||||||
vim.fn.confirm(path .. " is blocked.", "&Allow\n&Block\n&Ignore", 3)
|
path .. " is not allowed by direnv. What would you like to do?",
|
||||||
|
"&Allow\n&Block\n&Ignore",
|
||||||
|
1
|
||||||
|
)
|
||||||
|
|
||||||
if choice == 1 then
|
if choice == 1 then
|
||||||
M.allow_direnv()
|
M.allow_direnv()
|
||||||
M._init(path)
|
elseif choice == 2 then
|
||||||
end
|
M.deny_direnv()
|
||||||
|
|
||||||
if choice == 2 then
|
|
||||||
M._init(path)
|
|
||||||
end
|
end
|
||||||
|
-- Ignore means do nothing
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
M._get_rc_status(on_exit)
|
M._get_rc_status(on_exit)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Get direnv status for statusline integration
|
||||||
|
--- @return string status_string
|
||||||
|
M.statusline = function()
|
||||||
|
if not M.config.statusline.enabled then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
if cache.status == 0 then
|
||||||
|
return M.config.statusline.icon .. " active"
|
||||||
|
elseif cache.status == 1 then
|
||||||
|
return M.config.statusline.icon .. " pending"
|
||||||
|
elseif cache.status == 2 then
|
||||||
|
return M.config.statusline.icon .. " blocked"
|
||||||
|
else
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Setup the plugin with user configuration
|
||||||
|
--- @param user_config? table User configuration table
|
||||||
|
M.setup = function(user_config)
|
||||||
|
M.config = vim.tbl_deep_extend("force", {
|
||||||
|
bin = "direnv",
|
||||||
|
autoload_direnv = false,
|
||||||
|
statusline = {
|
||||||
|
enabled = false,
|
||||||
|
icon = "",
|
||||||
|
},
|
||||||
|
keybindings = {
|
||||||
|
allow = "<Leader>da",
|
||||||
|
deny = "<Leader>dd",
|
||||||
|
reload = "<Leader>dr",
|
||||||
|
edit = "<Leader>de",
|
||||||
|
},
|
||||||
|
notifications = {
|
||||||
|
level = vim.log.levels.INFO,
|
||||||
|
silent_autoload = true,
|
||||||
|
},
|
||||||
|
}, user_config or {})
|
||||||
|
|
||||||
|
if not check_executable(M.config.bin) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create user commands
|
||||||
|
vim.api.nvim_create_user_command("Direnv", function(opts)
|
||||||
|
local cmds = {
|
||||||
|
["allow"] = M.allow_direnv,
|
||||||
|
["deny"] = M.deny_direnv,
|
||||||
|
["reload"] = M.check_direnv,
|
||||||
|
["edit"] = M.edit_envrc,
|
||||||
|
["status"] = function()
|
||||||
|
M._get_rc_status(function(status, path)
|
||||||
|
if not path then
|
||||||
|
vim.schedule(function()
|
||||||
|
notify(
|
||||||
|
"No .envrc file found in current directory",
|
||||||
|
vim.log.levels.INFO
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local status_text = (status == 0 and "allowed")
|
||||||
|
or (status == 1 and "pending")
|
||||||
|
or (status == 2 and "blocked")
|
||||||
|
or "unknown"
|
||||||
|
|
||||||
|
vim.schedule(function()
|
||||||
|
notify(
|
||||||
|
"direnv status: " .. status_text .. " for " .. path,
|
||||||
|
vim.log.levels.INFO
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
local cmd = cmds[string.lower(opts.fargs[1])]
|
||||||
|
if cmd then
|
||||||
|
cmd()
|
||||||
|
else
|
||||||
|
notify(
|
||||||
|
"Unknown direnv command: " .. opts.fargs[1],
|
||||||
|
vim.log.levels.ERROR
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end, {
|
||||||
|
nargs = 1,
|
||||||
|
complete = function()
|
||||||
|
return { "allow", "deny", "reload", "edit", "status" }
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Setup keybindings
|
||||||
|
setup_keymaps({
|
||||||
|
{
|
||||||
|
M.config.keybindings.allow,
|
||||||
|
function()
|
||||||
|
M.allow_direnv()
|
||||||
|
end,
|
||||||
|
{ desc = "Allow direnv" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
M.config.keybindings.deny,
|
||||||
|
function()
|
||||||
|
M.deny_direnv()
|
||||||
|
end,
|
||||||
|
{ desc = "Deny direnv" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
M.config.keybindings.reload,
|
||||||
|
function()
|
||||||
|
M.check_direnv()
|
||||||
|
end,
|
||||||
|
{ desc = "Reload direnv" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
M.config.keybindings.edit,
|
||||||
|
function()
|
||||||
|
M.edit_envrc()
|
||||||
|
end,
|
||||||
|
{ desc = "Edit .envrc file" },
|
||||||
|
},
|
||||||
|
}, "n")
|
||||||
|
|
||||||
|
-- Check for .envrc files and set up autoload
|
||||||
|
local group_id = vim.api.nvim_create_augroup("DirenvNvim", { clear = true })
|
||||||
|
|
||||||
|
if M.config.autoload_direnv then
|
||||||
|
-- Check on directory change
|
||||||
|
vim.api.nvim_create_autocmd({ "DirChanged" }, {
|
||||||
|
group = group_id,
|
||||||
|
callback = function()
|
||||||
|
vim.b.direnv_autoload_triggered = true
|
||||||
|
M.check_direnv()
|
||||||
|
vim.defer_fn(function()
|
||||||
|
vim.b.direnv_autoload_triggered = false
|
||||||
|
end, 1000)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Check on startup if we're in a directory with .envrc
|
||||||
|
vim.api.nvim_create_autocmd({ "VimEnter" }, {
|
||||||
|
group = group_id,
|
||||||
|
callback = function()
|
||||||
|
vim.b.direnv_autoload_triggered = true
|
||||||
|
M.check_direnv()
|
||||||
|
-- Reset the flag after a short delay
|
||||||
|
vim.defer_fn(function()
|
||||||
|
vim.b.direnv_autoload_triggered = false
|
||||||
|
end, 1000)
|
||||||
|
end,
|
||||||
|
once = true,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check for .envrc changes
|
||||||
|
vim.api.nvim_create_autocmd({ "BufWritePost" }, {
|
||||||
|
pattern = ".envrc",
|
||||||
|
group = group_id,
|
||||||
|
callback = function()
|
||||||
|
cache.status = nil
|
||||||
|
notify(
|
||||||
|
".envrc file changed. Run :Direnv allow to activate changes.",
|
||||||
|
vim.log.levels.INFO
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Expose a command to refresh the statusline value without triggering reload
|
||||||
|
vim.api.nvim_create_user_command("DirenvStatuslineRefresh", function()
|
||||||
|
cache.last_check = 0
|
||||||
|
M._get_rc_status(function() end)
|
||||||
|
end, {})
|
||||||
|
|
||||||
|
M._get_rc_status(function() end)
|
||||||
|
|
||||||
|
process_notification_queue()
|
||||||
|
|
||||||
|
notify("direnv.nvim initialized", vim.log.levels.DEBUG)
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue