add block hierarchy plugin
authorLukas Hägele <lukas.haegele93@web.de>
Sun, 11 Oct 2026 11:33:58 +0000 (13:33 +0200)
committerLukas Hägele <lukas.haegele93@web.de>
Sun, 11 Oct 2026 11:33:58 +0000 (13:33 +0200)
plugin/block-hierarchy.lua [new file with mode: 0644]

diff --git a/plugin/block-hierarchy.lua b/plugin/block-hierarchy.lua
new file mode 100644 (file)
index 0000000..b6d57ca
--- /dev/null
@@ -0,0 +1,96 @@
+-- namespace
+local ns_id = vim.api.nvim_create_namespace("block_hierarchy")
+
+--- helper: get current blocks from treesitter
+--- @return { bufnr: integer, blocks: TSNode[]
+local function get_current_blocks()
+  local bufnr = vim.api.nvim_get_current_buf()
+
+  local ok, node = pcall(vim.treesitter.get_node)
+  if not ok or not node then
+    return nil, nil
+  end
+
+  local blocks = {}
+  local current = node
+  while current do
+    if current:type() == "compound_statement" then
+      table.insert(blocks, current)
+    end
+    current = current:parent()
+  end
+
+  return bufnr, blocks
+end
+
+--- update extmarks for current blocks
+local function update_extmarks()
+  local bufnr, blocks = get_current_blocks()
+  if not bufnr or not blocks then return end
+
+  -- delete old extmarks
+  vim.api.nvim_buf_clear_namespace(bufnr, ns_id, 0, -1)
+
+  for _, block in ipairs(blocks) do
+    local start_row, _, end_row, _ = block:range()
+    local header_row = math.max(0, start_row - 1)
+
+    local lines = vim.api.nvim_buf_get_lines(bufnr, header_row, header_row + 1, false)
+    local header_text = lines[1] and vim.trim(lines[1]) or ""
+
+    if header_text ~= "" then
+      pcall(vim.api.nvim_buf_set_extmark, bufnr, ns_id, end_row, 0, {
+        virt_text = { { header_text, "Comment" } },
+        virt_text_pos = "eol",
+        hl_mode = "combine",
+      })
+    end
+  end
+end
+
+--- show hierarchy in quickfix list
+local function show_block_hierarchy()
+  local bufnr, blocks = get_current_blocks()
+  if not bufnr or not blocks or #blocks == 0 then
+    vim.notify("no blocks at current cursor position", vim.log.levels.INFO)
+    return
+  end
+
+  local qf_items = {}
+
+  -- revert hierarchy
+  for i = #blocks, 1, -1 do
+    local block = blocks[i]
+    local start_row, _, _, _ = block:range()
+    local header_row = math.max(0, start_row - 1)
+
+    local lines = vim.api.nvim_buf_get_lines(bufnr, header_row, header_row + 1, false)
+    local header_text = lines[1] and vim.trim(lines[1]) or "[unknown block]"
+
+    table.insert(qf_items, {
+      bufnr = bufnr,
+      lnum = header_row + 1, -- quickfix expects 1-indexed lines
+      col = lines[1]:find("%S") or 1, -- find position of first non-whitespace character
+      text = header_text,
+    })
+  end
+
+  -- fill list and display
+  vim.fn.setqflist({}, "r", {
+    title = "block hierarchy",
+    items = qf_items,
+  })
+  vim.cmd("copen")
+end
+
+-- register user command: :BlockHierarchy
+vim.api.nvim_create_user_command("BlockHierarchy", show_block_hierarchy, {})
+
+-- register keymap
+vim.keymap.set("n", "<M-h>", ":BlockHierarchy<CR>", { silent = true, desc = "show block hierarchy" })
+
+-- update extmarks after cursor movement
+vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI", "BufEnter" }, {
+  pattern = {"*.c, *.h"},
+  callback = update_extmarks,
+})