--- /dev/null
+-- 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,
+})