From d455e89685f65e04309f57e80638d1a8051ec362 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sat, 11 Oct 2025 13:18:51 +0200 Subject: [PATCH 1/6] feat: add action highlight --- lua/helm-ls.lua | 18 ++++++- lua/helm-ls/action_highlight.lua | 89 ++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 lua/helm-ls/action_highlight.lua diff --git a/lua/helm-ls.lua b/lua/helm-ls.lua index e62b145..df7fb71 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 @@ -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..4413ae9 --- /dev/null +++ b/lua/helm-ls/action_highlight.lua @@ -0,0 +1,89 @@ +---@class ActionHighlightModule +local M = {} + +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_line = end_row, + end_col = end_col, + hl_group = "Visual", + }) +end + +local function highlight_keywords() + local bufnr = vim.api.nvim_get_current_buf() + local parser = vim.treesitter.get_parser(bufnr, "helm") + if not parser then + return + end + + local root = parser:parse()[1]:root() + local cursor_row, cursor_col = unpack(vim.api.nvim_win_get_cursor(0)) + cursor_row = cursor_row - 1 -- 0-indexed + + -- Clear previous highlights first + vim.api.nvim_buf_clear_namespace(bufnr, ns_id, 0, -1) + + local query_str = [[ + [ + (range_action "range" @start "end" @end) + (if_action "if" @start "end" @end) + (with_action "with" @start "end" @end) + ] @action + ]] + + local query = vim.treesitter.query.parse("helm", query_str) + if not query then + return + end + + -- Iterate over all matches, from innermost to outermost + for _, match in query:iter_matches(root, bufnr) do + local action_node + local start_node + local end_node + + for id, nodes in pairs(match) do + local capture_name = query.captures[id] + if capture_name == "action" then + action_node = nodes[1] + elseif capture_name == "start" then + start_node = nodes[1] + elseif capture_name == "end" then + end_node = nodes[1] + end + end + + if action_node and start_node and end_node then + local start_row, start_col, end_row, end_col = action_node:range() + + -- Check if cursor is within the action block's row range + if cursor_row >= start_row and cursor_row <= end_row then + -- Additional check for columns on the start and end lines to be more precise + if (cursor_row == start_row and cursor_col < start_col) or (cursor_row == end_row and cursor_col > end_col) then + -- Cursor is outside the node on the same line, so we continue searching for a parent block + else + -- Cursor is inside the block, highlight the keywords and stop. + highlight_node(bufnr, start_node) + highlight_node(bufnr, end_node) + return + end + end + end + end +end + +function M.setup(config) + -- Not needed for now +end + +function M.highlight_current_block() + highlight_keywords() +end + +return M From 97da0c4fbcab63ee6374a9342d5cc92150503d65 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sat, 11 Oct 2025 14:09:47 +0200 Subject: [PATCH 2/6] feat: jump between action block start/end --- README.md | 14 +++++- ftplugin/helm.lua | 8 ++++ lua/helm-ls/action_highlight.lua | 20 ++++----- lua/helm-ls/matchparen.lua | 73 ++++++++++++++++++++++++++++++++ lua/helm-ls/queries.lua | 13 ++++++ 5 files changed, 115 insertions(+), 13 deletions(-) create mode 100644 lua/helm-ls/matchparen.lua create mode 100644 lua/helm-ls/queries.lua diff --git a/README.md b/README.md index f3332f2..d774856 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`) +- 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/action_highlight.lua b/lua/helm-ls/action_highlight.lua index 4413ae9..7283d53 100644 --- a/lua/helm-ls/action_highlight.lua +++ b/lua/helm-ls/action_highlight.lua @@ -1,6 +1,8 @@ ---@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) @@ -29,15 +31,7 @@ local function highlight_keywords() -- Clear previous highlights first vim.api.nvim_buf_clear_namespace(bufnr, ns_id, 0, -1) - local query_str = [[ - [ - (range_action "range" @start "end" @end) - (if_action "if" @start "end" @end) - (with_action "with" @start "end" @end) - ] @action - ]] - - local query = vim.treesitter.query.parse("helm", query_str) + local query = vim.treesitter.query.parse("helm", queries.action_block) if not query then return end @@ -46,6 +40,7 @@ local function highlight_keywords() for _, match in query:iter_matches(root, bufnr) do local action_node local start_node + local middle_node local end_node for id, nodes in pairs(match) do @@ -54,6 +49,8 @@ local function highlight_keywords() action_node = nodes[1] elseif capture_name == "start" then start_node = nodes[1] + elseif capture_name == "middle" then + middle_node = nodes[1] elseif capture_name == "end" then end_node = nodes[1] end @@ -62,14 +59,15 @@ local function highlight_keywords() if action_node and start_node and end_node then local start_row, start_col, end_row, end_col = action_node:range() - -- Check if cursor is within the action block's row range if cursor_row >= start_row and cursor_row <= end_row then - -- Additional check for columns on the start and end lines to be more precise if (cursor_row == start_row and cursor_col < start_col) or (cursor_row == end_row and cursor_col > end_col) then -- Cursor is outside the node on the same line, so we continue searching for a parent block else -- Cursor is inside the block, highlight the keywords and stop. highlight_node(bufnr, start_node) + if middle_node then + highlight_node(bufnr, middle_node) + end highlight_node(bufnr, end_node) return end diff --git a/lua/helm-ls/matchparen.lua b/lua/helm-ls/matchparen.lua new file mode 100644 index 0000000..e9b4da2 --- /dev/null +++ b/lua/helm-ls/matchparen.lua @@ -0,0 +1,73 @@ +---@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 + end + + local root = parser:parse()[1]:root() + local cursor_row, cursor_col = unpack(vim.api.nvim_win_get_cursor(0)) + cursor_row = cursor_row - 1 -- 0-indexed + + local query = vim.treesitter.query.parse("helm", queries.action_block) + if not query then + return + end + + local matches = {} + for _, captures in query:iter_matches(root, bufnr) do + table.insert(matches, captures) + end + + -- Iterate backwards to find the innermost match first + for i = #matches, 1, -1 do + local match = matches[i] + local start_node + local middle_node + local end_node + + for id, nodes in pairs(match) do + local capture_name = query.captures[id] + if capture_name == "start" then + start_node = nodes[1] + elseif capture_name == "middle" then + middle_node = nodes[1] + elseif capture_name == "end" then + end_node = nodes[1] + end + end + + if start_node and end_node then + if is_cursor_on_node(cursor_row, cursor_col, start_node) then + local target_node = middle_node or end_node + local target_row, target_col = target_node:start() + vim.api.nvim_win_set_cursor(0, { target_row + 1, target_col }) + return true + elseif middle_node and is_cursor_on_node(cursor_row, cursor_col, middle_node) then + local end_start_row, end_start_col = end_node:start() + vim.api.nvim_win_set_cursor(0, { end_start_row + 1, end_start_col }) + return true + elseif is_cursor_on_node(cursor_row, cursor_col, end_node) then + local target_row, target_col = start_node:start() + vim.api.nvim_win_set_cursor(0, { target_row + 1, target_col }) + return true + end + end + 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..705dcbc --- /dev/null +++ b/lua/helm-ls/queries.lua @@ -0,0 +1,13 @@ +local M = {} + +M.action_block = [[ + [ + (range_action "range" @start ("else" @middle)? "end" @end) + (if_action "if" @start ("else" @middle)? ("else if" @middle)? "end" @end) + (with_action "with" @start ("else" @middle)? "end" @end) + (define_action "define" @start "end" @end) + (block_action "block" @start "end" @end) + ] @action +]] + +return M From 005655c9fb6607dea5ab506e1af3596f61fd5c90 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sun, 26 Oct 2025 16:18:39 +0100 Subject: [PATCH 3/6] fix: support multiple if else --- lua/helm-ls.lua | 2 +- lua/helm-ls/action_highlight.lua | 80 ++++++++++--------- lua/helm-ls/matchparen.lua | 128 ++++++++++++++++++++++--------- lua/helm-ls/queries.lua | 16 ++-- 4 files changed, 141 insertions(+), 85 deletions(-) diff --git a/lua/helm-ls.lua b/lua/helm-ls.lua index df7fb71..dbddb7c 100644 --- a/lua/helm-ls.lua +++ b/lua/helm-ls.lua @@ -75,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" }, { diff --git a/lua/helm-ls/action_highlight.lua b/lua/helm-ls/action_highlight.lua index 7283d53..ec533ac 100644 --- a/lua/helm-ls/action_highlight.lua +++ b/lua/helm-ls/action_highlight.lua @@ -11,7 +11,7 @@ local function highlight_node(bufnr, node) 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_line = end_row, + end_row = end_row, end_col = end_col, hl_group = "Visual", }) @@ -19,59 +19,57 @@ 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 - local root = parser:parse()[1]:root() - local cursor_row, cursor_col = unpack(vim.api.nvim_win_get_cursor(0)) - cursor_row = cursor_row - 1 -- 0-indexed - - -- Clear previous highlights first - vim.api.nvim_buf_clear_namespace(bufnr, ns_id, 0, -1) + -- Make sure tree is parsed. parse() is idempotent. + parser:parse() - local query = vim.treesitter.query.parse("helm", queries.action_block) - if not query then + local cursor_node = vim.treesitter.get_node({ bufnr = bufnr, include_anonymous = true }) + if not cursor_node then return end - -- Iterate over all matches, from innermost to outermost - for _, match in query:iter_matches(root, bufnr) do - local action_node - local start_node - local middle_node - local end_node - - for id, nodes in pairs(match) do - local capture_name = query.captures[id] - if capture_name == "action" then - action_node = nodes[1] - elseif capture_name == "start" then - start_node = nodes[1] - elseif capture_name == "middle" then - middle_node = nodes[1] - elseif capture_name == "end" then - end_node = nodes[1] - 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 action_node and start_node and end_node then - local start_row, start_col, end_row, end_col = action_node:range() + 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 - if cursor_row >= start_row and cursor_row <= end_row then - if (cursor_row == start_row and cursor_col < start_col) or (cursor_row == end_row and cursor_col > end_col) then - -- Cursor is outside the node on the same line, so we continue searching for a parent block - else - -- Cursor is inside the block, highlight the keywords and stop. - highlight_node(bufnr, start_node) - if middle_node then - highlight_node(bufnr, middle_node) - end - highlight_node(bufnr, end_node) - return - end + for id, node_to_highlight in parts_query:iter_captures(action_node, bufnr) 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 diff --git a/lua/helm-ls/matchparen.lua b/lua/helm-ls/matchparen.lua index e9b4da2..06c63bf 100644 --- a/lua/helm-ls/matchparen.lua +++ b/lua/helm-ls/matchparen.lua @@ -15,59 +15,117 @@ 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 + return false end + parser:parse() - local root = parser:parse()[1]:root() local cursor_row, cursor_col = unpack(vim.api.nvim_win_get_cursor(0)) - cursor_row = cursor_row - 1 -- 0-indexed + cursor_row = cursor_row - 1 - local query = vim.treesitter.query.parse("helm", queries.action_block) - if not query then - return + local cursor_node = vim.treesitter.get_node({ bufnr = bufnr, include_anonymous = true }) + if not cursor_node then + return false end - local matches = {} - for _, captures in query:iter_matches(root, bufnr) do - table.insert(matches, captures) + -- 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 false end - -- Iterate backwards to find the innermost match first - for i = #matches, 1, -1 do - local match = matches[i] - local start_node - local middle_node - local end_node + -- 2. Get all parts, filter nested, and identify node under cursor + local parts_query = vim.treesitter.query.parse("helm", queries.action_parts) + if not parts_query then + return false + end - for id, nodes in pairs(match) do - local capture_name = query.captures[id] + local start_nodes, middle_nodes, end_nodes = {}, {}, {} + local cursor_on_node + + for id, node in parts_query:iter_captures(action_node, bufnr) 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 + if is_cursor_on_node(cursor_row, cursor_col, node) then + cursor_on_node = node + end + local capture_name = parts_query.captures[id] if capture_name == "start" then - start_node = nodes[1] + table.insert(start_nodes, node) elseif capture_name == "middle" then - middle_node = nodes[1] + table.insert(middle_nodes, node) elseif capture_name == "end" then - end_node = nodes[1] + table.insert(end_nodes, node) end end + end + + if not cursor_on_node then + return false + end + + -- Sort nodes by position + table.sort(start_nodes, function(a, b) return a:start() < b:start() end) + table.sort(middle_nodes, function(a, b) return a:start() < b:start() end) + table.sort(end_nodes, function(a, b) return a:start() < b:start() end) - if start_node and end_node then - if is_cursor_on_node(cursor_row, cursor_col, start_node) then - local target_node = middle_node or end_node - local target_row, target_col = target_node:start() - vim.api.nvim_win_set_cursor(0, { target_row + 1, target_col }) - return true - elseif middle_node and is_cursor_on_node(cursor_row, cursor_col, middle_node) then - local end_start_row, end_start_col = end_node:start() - vim.api.nvim_win_set_cursor(0, { end_start_row + 1, end_start_col }) - return true - elseif is_cursor_on_node(cursor_row, cursor_col, end_node) then - local target_row, target_col = start_node:start() - vim.api.nvim_win_set_cursor(0, { target_row + 1, target_col }) - return true + -- 3. Find which list the cursor_on_node is in and jump + local function jump_to(node) + local r, c = node:start() + vim.api.nvim_win_set_cursor(0, { r + 1, c }) + return true + end + + for i, node in ipairs(start_nodes) do + if node:id() == cursor_on_node:id() then + if #middle_nodes > 0 then + return jump_to(middle_nodes[1]) + elseif #end_nodes > 0 then + return jump_to(end_nodes[1]) + end + return false + end + end + + for i, node in ipairs(middle_nodes) do + if node:id() == cursor_on_node:id() then + if i + 1 <= #middle_nodes then + return jump_to(middle_nodes[i + 1]) + elseif #end_nodes > 0 then + return jump_to(end_nodes[1]) + end + return false + end + end + + for i, node in ipairs(end_nodes) do + if node:id() == cursor_on_node:id() then + if #start_nodes > 0 then + return jump_to(start_nodes[1]) end + return false end end + return false end -return M +return M \ No newline at end of file diff --git a/lua/helm-ls/queries.lua b/lua/helm-ls/queries.lua index 705dcbc..25e49e4 100644 --- a/lua/helm-ls/queries.lua +++ b/lua/helm-ls/queries.lua @@ -1,13 +1,13 @@ local M = {} -M.action_block = [[ - [ - (range_action "range" @start ("else" @middle)? "end" @end) - (if_action "if" @start ("else" @middle)? ("else if" @middle)? "end" @end) - (with_action "with" @start ("else" @middle)? "end" @end) - (define_action "define" @start "end" @end) - (block_action "block" @start "end" @end) - ] @action +M.action_parts = [[ + ("range" @start) + ("if" @start) + ("with" @start) + ("define" @start) + ("block" @start) + (["else" "else if"] @middle) + ("end" @end) ]] return M From 48aa3c04fcfa067e8c22b2ee7850cc680672ee77 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sun, 26 Oct 2025 16:23:43 +0100 Subject: [PATCH 4/6] chore: cleanup --- lua/helm-ls.lua | 2 +- ...ion_highlight.lua => action-highlight.lua} | 6 ++- lua/helm-ls/matchparen.lua | 52 +++++++------------ 3 files changed, 25 insertions(+), 35 deletions(-) rename lua/helm-ls/{action_highlight.lua => action-highlight.lua} (92%) diff --git a/lua/helm-ls.lua b/lua/helm-ls.lua index dbddb7c..d84a8f4 100644 --- a/lua/helm-ls.lua +++ b/lua/helm-ls.lua @@ -55,7 +55,7 @@ M.setup = function(args) end if M.config.action_highlight.enabled then - action_highlight = require("helm-ls.action_highlight") + action_highlight = require("helm-ls.action-highlight") action_highlight.setup(M.config.action_highlight) end diff --git a/lua/helm-ls/action_highlight.lua b/lua/helm-ls/action-highlight.lua similarity index 92% rename from lua/helm-ls/action_highlight.lua rename to lua/helm-ls/action-highlight.lua index ec533ac..7ee2b14 100644 --- a/lua/helm-ls/action_highlight.lua +++ b/lua/helm-ls/action-highlight.lua @@ -56,7 +56,11 @@ local function highlight_keywords() return end - for id, node_to_highlight in parts_query:iter_captures(action_node, bufnr) do + -- 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 diff --git a/lua/helm-ls/matchparen.lua b/lua/helm-ls/matchparen.lua index 06c63bf..ec170ed 100644 --- a/lua/helm-ls/matchparen.lua +++ b/lua/helm-ls/matchparen.lua @@ -27,7 +27,7 @@ function M.jump_to_matching_keyword() return false end - -- 1. Find the containing action node by traversing up from cursor + -- 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" } @@ -43,14 +43,14 @@ function M.jump_to_matching_keyword() return false end - -- 2. Get all parts, filter nested, and identify node under cursor + -- 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 + local cursor_on_node, cursor_on_node_type for id, node in parts_query:iter_captures(action_node, bufnr) do local is_nested = false @@ -64,10 +64,11 @@ function M.jump_to_matching_keyword() 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 - local capture_name = parts_query.captures[id] if capture_name == "start" then table.insert(start_nodes, node) elseif capture_name == "middle" then @@ -87,45 +88,30 @@ function M.jump_to_matching_keyword() table.sort(middle_nodes, function(a, b) return a:start() < b:start() end) table.sort(end_nodes, function(a, b) return a:start() < b:start() end) - -- 3. Find which list the cursor_on_node is in and jump + -- 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 - for i, node in ipairs(start_nodes) do - if node:id() == cursor_on_node:id() then - if #middle_nodes > 0 then - return jump_to(middle_nodes[1]) - elseif #end_nodes > 0 then - return jump_to(end_nodes[1]) + 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 - return false - end - end - - for i, node in ipairs(middle_nodes) do - if node:id() == cursor_on_node:id() then - if i + 1 <= #middle_nodes then - return jump_to(middle_nodes[i + 1]) - elseif #end_nodes > 0 then - return jump_to(end_nodes[1]) - end - return false - end - end - - for i, node in ipairs(end_nodes) do - if node:id() == cursor_on_node:id() then - if #start_nodes > 0 then - return jump_to(start_nodes[1]) - end - return false end + elseif cursor_on_node_type == "end" then + return jump_to(start_nodes[1]) end return false end -return M \ No newline at end of file +return M From f230b1917f2017fed1d51afd91f773fc477b4681 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sun, 26 Oct 2025 16:29:33 +0100 Subject: [PATCH 5/6] chore: mr comments --- lua/helm-ls/matchparen.lua | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lua/helm-ls/matchparen.lua b/lua/helm-ls/matchparen.lua index ec170ed..4d27ef0 100644 --- a/lua/helm-ls/matchparen.lua +++ b/lua/helm-ls/matchparen.lua @@ -52,7 +52,8 @@ function M.jump_to_matching_keyword() local start_nodes, middle_nodes, end_nodes = {}, {}, {} local cursor_on_node, cursor_on_node_type - for id, node in parts_query:iter_captures(action_node, bufnr) do + 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 @@ -84,9 +85,14 @@ function M.jump_to_matching_keyword() end -- Sort nodes by position - table.sort(start_nodes, function(a, b) return a:start() < b:start() end) - table.sort(middle_nodes, function(a, b) return a:start() < b:start() end) - table.sort(end_nodes, function(a, b) return a:start() < b:start() end) + 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) From 6d6f49df6fba3365e76d0d0ab1cfc12b34e50c87 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sun, 26 Oct 2025 16:33:50 +0100 Subject: [PATCH 6/6] docs: clarify action types --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d774856..ca7e5c4 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ 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`) +- 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))