diff --git a/README.md b/README.md index f3332f2..ca7e5c4 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,17 @@ The plugin is in early development. ## Features - File types for Helm (including values.yaml files required for helm-ls) - +- Highlight the current block (`if`, `with`, `range`, etc.) +- Jump between the start and end of a block with `%` - experimental: Overwrite templates with their current values using virtual text (See [Demos](#demos)) - - experimental: Show hints highlighting the effect of `nindent` and `indent` functions (See [Demos](#demos)) +## Keymaps + +The plugin adds the following keymaps for helm files: + +- `%`: Jump between the start and end of a block (`if`, `with`, `range`) + ## Installing ### Using `lazy.nvim` @@ -54,6 +60,10 @@ Default config: -- show the hints only for the line the cursor is on only_for_current_line = true, }, + action_highlight = { + -- enable highlighting of the current block + enabled = true, + }, } ``` diff --git a/ftplugin/helm.lua b/ftplugin/helm.lua index 47025ca..c093a94 100644 --- a/ftplugin/helm.lua +++ b/ftplugin/helm.lua @@ -1,2 +1,10 @@ -- set up the gotmpl commentstring vim.opt_local.commentstring = "{{/* %s */}}" + +vim.keymap.set("n", "%", function() + local jumped = require("helm-ls.matchparen").jump_to_matching_keyword() + if not jumped then + -- Fallback to default % behavior + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("%", true, false, true), "n", false) + end +end, { buffer = true, noremap = true, silent = true, desc = "Jump to matching keyword" }) diff --git a/lua/helm-ls.lua b/lua/helm-ls.lua index e62b145..d84a8f4 100644 --- a/lua/helm-ls.lua +++ b/lua/helm-ls.lua @@ -3,6 +3,7 @@ ---@class Config ---@field conceal_templates table ---@field indent_hints table +---@field action_highlight table local config = { conceal_templates = { enabled = true, @@ -11,6 +12,9 @@ local config = { enabled = true, only_for_current_line = true, }, + action_highlight = { + enabled = true, + }, } ---@class MyModule @@ -29,12 +33,16 @@ M.setup = function(args) if args.indent_hints and type(args.indent_hints) ~= "table" then error("Helm-ls: Invalid type for indent_hints in config") end + if args.action_highlight and type(args.action_highlight) ~= "table" then + error("Helm-ls: Invalid type for action_highlight in config") + end end M.config = vim.tbl_deep_extend("force", M.config, args or {}) local conceal = nil local indent_hints = nil + local action_highlight = nil if M.config.conceal_templates.enabled then conceal = require("helm-ls.conceal") @@ -46,7 +54,12 @@ M.setup = function(args) indent_hints.set_config(M.config.indent_hints) end - if not conceal and not indent_hints then + if M.config.action_highlight.enabled then + action_highlight = require("helm-ls.action-highlight") + action_highlight.setup(M.config.action_highlight) + end + + if not conceal and not indent_hints and not action_highlight then -- create no autocommand as the features are disabled return end @@ -62,7 +75,7 @@ M.setup = function(args) local group_id = vim.api.nvim_create_augroup("helm-ls.nvim", { clear = true }) -- Define file patterns as constants - local file_patterns = { "*.yaml", "*.yml", "*.helm", "*.tpl" } + local file_patterns = { "*.yaml", "*.yml", "*.helm", "*.tpl", "NOTES.txt" } -- Define the autocommand vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, { @@ -78,6 +91,9 @@ M.setup = function(args) if conceal then conceal.update_conceal_templates() end + if action_highlight then + action_highlight.highlight_current_block() + end end, }) end diff --git a/lua/helm-ls/action-highlight.lua b/lua/helm-ls/action-highlight.lua new file mode 100644 index 0000000..7ee2b14 --- /dev/null +++ b/lua/helm-ls/action-highlight.lua @@ -0,0 +1,89 @@ +---@class ActionHighlightModule +local M = {} + +local queries = require("helm-ls.queries") + +local ns_id = vim.api.nvim_create_namespace("helm-ls-action-highlight") + +local function highlight_node(bufnr, node) + if not node then + return + end + local start_row, start_col, end_row, end_col = node:range() + vim.api.nvim_buf_set_extmark(bufnr, ns_id, start_row, start_col, { + end_row = end_row, + end_col = end_col, + hl_group = "Visual", + }) +end + +local function highlight_keywords() + local bufnr = vim.api.nvim_get_current_buf() + vim.api.nvim_buf_clear_namespace(bufnr, ns_id, 0, -1) + + local parser = vim.treesitter.get_parser(bufnr, "helm") + if not parser then + return + end + + -- Make sure tree is parsed. parse() is idempotent. + parser:parse() + + local cursor_node = vim.treesitter.get_node({ bufnr = bufnr, include_anonymous = true }) + if not cursor_node then + return + end + + -- 1. Find the containing action node by traversing up from cursor + local action_node + local current_node = cursor_node + local action_types = { "range_action", "if_action", "with_action", "define_action", "block_action" } + while current_node do + if vim.tbl_contains(action_types, current_node:type()) then + action_node = current_node + break + end + current_node = current_node:parent() + end + + if not action_node then + return -- No action found at cursor + end + + -- 2. Find parts within that action node and highlight them + local parts_query = vim.treesitter.query.parse("helm", queries.action_parts) + if not parts_query then + return + end + + -- Get the visible range of lines in the current window + local start_line = vim.fn.line("w0") - 1 + local end_line = vim.fn.line("w$") - 1 + + for id, node_to_highlight in parts_query:iter_captures(action_node, bufnr, start_line, end_line) do + local is_nested = false + local parent = node_to_highlight:parent() + -- Check if the capture is inside a nested action block + while parent and parent:id() ~= action_node:id() do + if vim.tbl_contains(action_types, parent:type()) then + is_nested = true + break + end + parent = parent:parent() + end + + if not is_nested then + highlight_node(bufnr, node_to_highlight) + end + end +end + +function M.setup(config) + -- Not needed for now +end + +function M.highlight_current_block() + highlight_keywords() +end + +return M diff --git a/lua/helm-ls/matchparen.lua b/lua/helm-ls/matchparen.lua new file mode 100644 index 0000000..4d27ef0 --- /dev/null +++ b/lua/helm-ls/matchparen.lua @@ -0,0 +1,123 @@ +---@class MatchParenModule +local M = {} + +local queries = require("helm-ls.queries") + +local function is_cursor_on_node(cursor_row, cursor_col, node) + local start_row, start_col, _, end_col = node:range() + if cursor_row == start_row and cursor_col >= start_col and cursor_col < end_col then + return true + end + return false +end + +function M.jump_to_matching_keyword() + local bufnr = vim.api.nvim_get_current_buf() + local parser = vim.treesitter.get_parser(bufnr, "helm") + if not parser then + return false + end + parser:parse() + + local cursor_row, cursor_col = unpack(vim.api.nvim_win_get_cursor(0)) + cursor_row = cursor_row - 1 + + local cursor_node = vim.treesitter.get_node({ bufnr = bufnr, include_anonymous = true }) + if not cursor_node then + return false + end + + -- 1. Find the containing action node + local action_node + local current_node = cursor_node + local action_types = { "range_action", "if_action", "with_action", "define_action", "block_action" } + while current_node do + if vim.tbl_contains(action_types, current_node:type()) then + action_node = current_node + break + end + current_node = current_node:parent() + end + + if not action_node then + return false + end + + -- 2. Collect parts and identify node under cursor in a single pass + local parts_query = vim.treesitter.query.parse("helm", queries.action_parts) + if not parts_query then + return false + end + + local start_nodes, middle_nodes, end_nodes = {}, {}, {} + local cursor_on_node, cursor_on_node_type + + local start_row, _, end_row, _ = action_node:range() + for id, node in parts_query:iter_captures(action_node, bufnr, start_row, end_row + 1) do + local is_nested = false + local parent = node:parent() + while parent and parent:id() ~= action_node:id() do + if vim.tbl_contains(action_types, parent:type()) then + is_nested = true + break + end + parent = parent:parent() + end + + if not is_nested then + local capture_name = parts_query.captures[id] + if is_cursor_on_node(cursor_row, cursor_col, node) then + cursor_on_node = node + cursor_on_node_type = capture_name + end + if capture_name == "start" then + table.insert(start_nodes, node) + elseif capture_name == "middle" then + table.insert(middle_nodes, node) + elseif capture_name == "end" then + table.insert(end_nodes, node) + end + end + end + + if not cursor_on_node then + return false + end + + -- Sort nodes by position + local function by_pos(a, b) + local ar, ac = a:start() + local br, bc = b:start() + return ar == br and ac < bc or ar < br + end + table.sort(start_nodes, by_pos) + table.sort(middle_nodes, by_pos) + table.sort(end_nodes, by_pos) + + -- 3. Jump based on the type of node under the cursor + local function jump_to(node) + if not node then + return false + end + local r, c = node:start() + vim.api.nvim_win_set_cursor(0, { r + 1, c }) + return true + end + + if cursor_on_node_type == "start" then + return jump_to(middle_nodes[1]) or jump_to(end_nodes[1]) + elseif cursor_on_node_type == "middle" then + -- To find the next middle node, we must find the index of the current one + for i, node in ipairs(middle_nodes) do + if node:id() == cursor_on_node:id() then + return jump_to(middle_nodes[i + 1]) or jump_to(end_nodes[1]) + end + end + elseif cursor_on_node_type == "end" then + return jump_to(start_nodes[1]) + end + + return false +end + +return M diff --git a/lua/helm-ls/queries.lua b/lua/helm-ls/queries.lua new file mode 100644 index 0000000..25e49e4 --- /dev/null +++ b/lua/helm-ls/queries.lua @@ -0,0 +1,13 @@ +local M = {} + +M.action_parts = [[ + ("range" @start) + ("if" @start) + ("with" @start) + ("define" @start) + ("block" @start) + (["else" "else if"] @middle) + ("end" @end) +]] + +return M