Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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,
},
}
```

Expand Down
8 changes: 8 additions & 0 deletions ftplugin/helm.lua
Original file line number Diff line number Diff line change
@@ -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" })
20 changes: 18 additions & 2 deletions lua/helm-ls.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
---@class Config
---@field conceal_templates table
---@field indent_hints table
---@field action_highlight table
local config = {
conceal_templates = {
enabled = true,
Expand All @@ -11,6 +12,9 @@ local config = {
enabled = true,
only_for_current_line = true,
},
action_highlight = {
enabled = true,
},
}

---@class MyModule
Expand All @@ -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")
Expand All @@ -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
Expand All @@ -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" }, {
Expand All @@ -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
Expand Down
89 changes: 89 additions & 0 deletions lua/helm-ls/action-highlight.lua
Original file line number Diff line number Diff line change
@@ -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
123 changes: 123 additions & 0 deletions lua/helm-ls/matchparen.lua
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions lua/helm-ls/queries.lua
Original file line number Diff line number Diff line change
@@ -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