From 691bcef6844713b97b80806bc80e6029a0e5e5a1 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 22 Oct 2025 12:18:04 +0300 Subject: [PATCH] direnv: init and populate 'integrations' namespace; add TS highlighting Signed-off-by: NotAShelf Change-Id: I78cd545cb9c3b85d7405e9f4723f15066a6a6964 --- lua/direnv.lua | 127 ++++++++++-- lua/direnv/integrations/init.lua | 9 + lua/direnv/integrations/statusline.lua | 41 ++++ lua/direnv/integrations/treesitter.lua | 259 +++++++++++++++++++++++++ 4 files changed, 418 insertions(+), 18 deletions(-) create mode 100644 lua/direnv/integrations/init.lua create mode 100644 lua/direnv/integrations/statusline.lua create mode 100644 lua/direnv/integrations/treesitter.lua diff --git a/lua/direnv.lua b/lua/direnv.lua index b3448c7..548387c 100644 --- a/lua/direnv.lua +++ b/lua/direnv.lua @@ -4,9 +4,6 @@ local M = {} --- @field bin string Path to direnv executable --- @field autoload_direnv boolean Automatically load direnv when opening files --- @field cache_ttl integer Cache TTL in milliseconds for direnv status checks ---- @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 @@ -15,6 +12,16 @@ local M = {} --- @field notifications table Notification settings --- @field notifications.level integer Log level for notifications --- @field notifications.silent_autoload boolean Don't show notifications during autoload and initialization +--- @field integrations table Integration configurations +--- @field integrations.statusline table Statusline configuration +--- @field integrations.statusline.enabled boolean Enable statusline integration +--- @field integrations.statusline.icon string Icon to show in statusline +--- @field integrations.treesitter table Treesitter configuration +--- @field integrations.treesitter.enabled boolean Enable Treesitter syntax highlighting for .envrc files +--- +--- The direnv.nvim plugin provides integrations with other Neovim plugins: +--- - direnv.integrations.treesitter: Syntax highlighting for .envrc files +--- - direnv.integrations.statusline: Statusline component for direnv status local cache = { status = nil, @@ -26,6 +33,28 @@ local cache = { local notification_queue = {} local pending_callbacks = {} +-- Lazy load integration modules +local treesitter = nil +local statusline = nil + +local function get_treesitter() + if treesitter == nil then + treesitter = pcall(require, "direnv.integrations.treesitter") + and require("direnv.integrations.treesitter") + or nil + end + return treesitter +end + +local function get_statusline() + if statusline == nil then + statusline = pcall(require, "direnv.integrations.statusline") + and require("direnv.integrations.statusline") + or nil + end + return statusline +end + --- Check if an executable is available in PATH --- @param executable_name string Name of the executable --- @return boolean is_available @@ -173,6 +202,12 @@ M._get_rc_status = function(callback) cache.status = nil cache.path = nil cache.last_check = now + + -- Update statusline integration + local sl = get_statusline() + if sl then + sl.update_status(cache.status) + end for _, cb in ipairs(pending_callbacks) do cb(nil, nil) end @@ -184,6 +219,12 @@ M._get_rc_status = function(callback) cache.path = status.state.foundRC.path cache.last_check = now + -- Update statusline integration + local sl = get_statusline() + if sl then + sl.update_status(cache.status) + end + for _, cb in ipairs(pending_callbacks) do cb(status.state.foundRC.allowed, status.state.foundRC.path) end @@ -293,6 +334,13 @@ M.allow_direnv = function() -- Clear cache to ensure we get fresh data -- and then load the environment cache.status = nil + + -- Update statusline integration + local sl = get_statusline() + if sl then + sl.update_status(cache.status) + end + M.check_direnv() vim.schedule(function() @@ -341,6 +389,12 @@ M.deny_direnv = function() cache.status = nil + -- Update statusline integration + local sl = get_statusline() + if sl then + sl.update_status(cache.status) + end + vim.schedule(function() notify("direnv denied for " .. path, vim.log.levels.INFO) end) @@ -370,6 +424,15 @@ M.edit_envrc = function() if create_new == 1 then vim.cmd("edit " .. envrc_path) + -- Setup Treesitter for newly created .envrc file + if M.config.integrations.treesitter.enabled then + local ts = get_treesitter() + if ts then + vim.defer_fn(function() + ts.setup_buffer() + end, 100) + end + end end end) return @@ -377,6 +440,15 @@ M.edit_envrc = function() vim.schedule(function() vim.cmd("edit " .. path) + -- Setup Treesitter for existing .envrc file + if M.config.integrations.treesitter.enabled then + local ts = get_treesitter() + if ts then + vim.defer_fn(function() + ts.setup_buffer() + end, 100) + end + end end) end) end @@ -427,19 +499,11 @@ 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 + local sl = get_statusline() + if not sl then return "" end + return sl.statusline(M.config.integrations.statusline) end --- Setup the plugin with user configuration @@ -449,10 +513,6 @@ M.setup = function(user_config) bin = "direnv", autoload_direnv = false, cache_ttl = 5000, - statusline = { - enabled = false, - icon = "󱚟", - }, keybindings = { allow = "da", deny = "dd", @@ -463,6 +523,15 @@ M.setup = function(user_config) level = vim.log.levels.INFO, silent_autoload = true, }, + integrations = { + statusline = { + enabled = false, + icon = "", + }, + treesitter = { + enabled = true, + }, + }, }, user_config or {}) if not check_executable(M.config.bin) then @@ -588,6 +657,13 @@ M.setup = function(user_config) group = group_id, callback = function() cache.status = nil + + -- Update statusline integration + local sl = get_statusline() + if sl then + sl.update_status(cache.status) + end + notify( ".envrc file changed. Run :Direnv allow to activate changes.", vim.log.levels.INFO @@ -598,6 +674,13 @@ M.setup = function(user_config) -- Expose a command to refresh the statusline value without triggering reload vim.api.nvim_create_user_command("DirenvStatuslineRefresh", function() cache.last_check = 0 + + -- Refresh statusline integration + local sl = get_statusline() + if sl then + sl.refresh() + end + M._get_rc_status(function() end) end, {}) @@ -605,6 +688,14 @@ M.setup = function(user_config) process_notification_queue() + -- Setup Treesitter support if enabled + if M.config.integrations.treesitter.enabled then + local ts = get_treesitter() + if ts then + ts.setup(M.config) + end + end + if not M.config.notifications.silent_autoload then notify("direnv.nvim initialized", vim.log.levels.DEBUG) end diff --git a/lua/direnv/integrations/init.lua b/lua/direnv/integrations/init.lua new file mode 100644 index 0000000..76b6ab9 --- /dev/null +++ b/lua/direnv/integrations/init.lua @@ -0,0 +1,9 @@ +local M = {} + +-- Treesitter integration +M.treesitter = require("direnv.integrations.treesitter") + +-- Statusline integration +M.statusline = require("direnv.integrations.statusline") + +return M diff --git a/lua/direnv/integrations/statusline.lua b/lua/direnv/integrations/statusline.lua new file mode 100644 index 0000000..df8a2f7 --- /dev/null +++ b/lua/direnv/integrations/statusline.lua @@ -0,0 +1,41 @@ +local M = {} + +-- Cache for statusline data +local cache = { + status = nil, + last_check = 0, +} + +-- Get direnv status for statusline integration +--- @param config table Direnv configuration +--- @return string status_string +M.statusline = function(config) + local statusline_config = config.integrations and config.integrations.statusline or config.statusline + + if not statusline_config or not statusline_config.enabled then + return "" + end + + if cache.status == 0 then + return statusline_config.icon .. " active" + elseif cache.status == 1 then + return statusline_config.icon .. " pending" + elseif cache.status == 2 then + return statusline_config.icon .. " blocked" + else + return "" + end +end + +-- Update the cached status from the main module +--- @param status number|nil The direnv status +M.update_status = function(status) + cache.status = status +end + +-- Refresh the statusline cache +M.refresh = function() + cache.last_check = 0 +end + +return M \ No newline at end of file diff --git a/lua/direnv/integrations/treesitter.lua b/lua/direnv/integrations/treesitter.lua new file mode 100644 index 0000000..53cd68f --- /dev/null +++ b/lua/direnv/integrations/treesitter.lua @@ -0,0 +1,259 @@ +local M = {} + +-- Direnv keyword groups based on syntax.vim +local direnv_keywords = { + -- Command functions (takes CLI command argument) + command_funcs = { + "has", + }, + + -- Path functions (takes file/dir path argument) + path_funcs = { + "dotenv", + "dotenv_if_exists", + "env_vars_required", + "fetchurl", + "join_args", + "user_rel_path", + "on_git_branch", + "find_up", + "has", + "source_env", + "source_env_if_exists", + "source_up", + "source_up_if_exists", + "source_url", + "PATH_add", + "MANPATH_add", + "load_prefix", + "watch_file", + "watch_dir", + "semver_search", + "strict_env", + "unstrict_env", + }, + + -- Expand path functions + expand_path_funcs = { + "expand_path", + }, + + -- Path add functions (takes variable name and dir path) + path_add_funcs = { + "PATH_add", + "MANPATH_add", + "PATH_rm", + "path_rm", + "path_add", + }, + + -- Use functions + use_funcs = { + "use", + "use_flake", + "use_guix", + "use_julia", + "use_nix", + "use_node", + "use_nodenv", + "use_rbenv", + "use_vim", + "rvm", + }, + + -- Layout functions + layout_funcs = { + "layout", + "layout_anaconda", + "layout_go", + "layout_julia", + "layout_node", + "layout_perl", + "layout_php", + "layout_pipenv", + "layout_pyenv", + "layout_python", + "layout_python2", + "layout_python3", + "layout_ruby", + }, + + -- Layout languages + layout_languages = { + "go", + "node", + "perl", + "python3", + "ruby", + }, + + -- Layout language paths + layout_language_paths = { + "python", + }, + + -- Other functions + other_funcs = { + "direnv_apply_dump", + "direnv_layout_dir", + "direnv_load", + "direnv_version", + "log_error", + "log_status", + }, +} + +-- Check if Treesitter is available +local function has_treesitter() + return vim.fn.has("nvim-0.8.0") == 1 and pcall(require, "nvim-treesitter") +end + +-- Check if bash parser is available +local function has_bash_parser() + local ok, parsers = pcall(require, "nvim-treesitter.parsers") + return ok and parsers.has_parser("bash") +end + +-- Setup Treesitter highlighting for direnv +M.setup = function(config) + if not config.integrations.treesitter.enabled then + return + end + + if not has_treesitter() then + return + end + + if not has_bash_parser() then + return + end + + -- Create highlight groups + local highlights = { + -- Command functions + ["@direnv.command_func"] = { link = "Function" }, + ["@direnv.command"] = { link = "Identifier" }, + + -- Path functions + ["@direnv.path_func"] = { link = "Function" }, + ["@direnv.path"] = { link = "Directory" }, + ["@direnv.expand_path_func"] = { link = "Function" }, + ["@direnv.expand_path_rel"] = { link = "Directory" }, + ["@direnv.path_add_func"] = { link = "Function" }, + ["@direnv.var"] = { link = "Identifier" }, + + -- Use functions + ["@direnv.use_func"] = { link = "Function" }, + ["@direnv.use_command"] = { link = "Identifier" }, + + -- Layout functions + ["@direnv.layout_func"] = { link = "Function" }, + ["@direnv.layout_language"] = { link = "Identifier" }, + ["@direnv.layout_language_path"] = { link = "Identifier" }, + + -- Other functions + ["@direnv.func"] = { link = "Function" }, + } + + for group, hl in pairs(highlights) do + vim.api.nvim_set_hl(0, group, hl) + end + + -- Setup autocmd for .envrc files + local group = vim.api.nvim_create_augroup("DirenvTreesitter", { clear = true }) + + vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, { + group = group, + pattern = ".envrc", + callback = function() + M.setup_buffer() + end, + }) +end + +-- Setup Treesitter highlighting for current buffer +M.setup_buffer = function() + if not has_treesitter() or not has_bash_parser() then + return + end + + local bufnr = vim.api.nvim_get_current_buf() + local ft = vim.bo[bufnr].filetype + + -- Only apply to .envrc files or bash files + if ft ~= "sh" and ft ~= "bash" and not vim.fn.expand("%:t"):match("%.envrc$") then + return + end + + -- Set filetype to bash if it's .envrc + if vim.fn.expand("%:t"):match("%.envrc$") then + vim.bo[bufnr].filetype = "bash" + end + + -- Use the proper Treesitter API for highlighting + local ok, ts = pcall(require, "vim.treesitter") + if not ok then + return + end + + -- Create queries for direnv keywords + local query = M.create_queries() + + -- Add the queries to the buffer + ts.query.set("bash", "highlights", query) + + -- Force Treesitter to re-parse and re-highlight the buffer + vim.schedule(function() + local parser = ts.get_parser(bufnr, "bash") + if parser then + parser:parse() + end + end) +end + +-- Create Treesitter queries for direnv keywords +M.create_queries = function() + local patterns = {} + + -- Create query patterns for each keyword group + local function create_pattern(keywords, capture_name) + for _, keyword in ipairs(keywords) do + table.insert(patterns, string.format( + '((call_expression function: (identifier) @%s) (#eq? @%s "%s"))', + capture_name, capture_name, keyword + )) + end + end + + -- Add patterns for all keyword groups + create_pattern(direnv_keywords.command_funcs, "direnv.command_func") + create_pattern(direnv_keywords.path_funcs, "direnv.path_func") + create_pattern(direnv_keywords.expand_path_funcs, "direnv.expand_path_func") + create_pattern(direnv_keywords.path_add_funcs, "direnv.path_add_func") + create_pattern(direnv_keywords.use_funcs, "direnv.use_func") + create_pattern(direnv_keywords.layout_funcs, "direnv.layout_func") + create_pattern(direnv_keywords.other_funcs, "direnv.func") + + -- Layout languages + create_pattern(direnv_keywords.layout_languages, "direnv.layout_language") + create_pattern(direnv_keywords.layout_language_paths, "direnv.layout_language_path") + + return table.concat(patterns, "\n") +end + +-- Get all direnv keywords for completion +M.get_keywords = function() + local all_keywords = {} + + for _, group in pairs(direnv_keywords) do + if type(group) == "table" then + for _, keyword in ipairs(group) do + table.insert(all_keywords, keyword) + end + end + end + + return all_keywords +end + +return M \ No newline at end of file