feat: add interactive attribute block toggle with keybindings and commands

Add ability to expand/collapse SCL variable attribute blocks like {EXTERNALACCESSIBLE := 'false'} to {...}

Features:
- Per-variable collapse rules via collapseVariableRules option
- Interactive toggle with <Leader>xa keybinding
- Commands: SCLToggleAttrBlock, SCLExpandAllAttrBlocks, SCLCollapseAllAttrBlocks
- Keybindings: <Leader>xa (toggle), <Leader>xae (expand all), <Leader>xac (collapse all)
- Supports both formatter-collapsed and manually collapsed blocks

Also update AGENTS.md with new documentation and troubleshooting guide
This commit is contained in:
2026-02-19 10:12:58 +01:00
parent 246fa85442
commit 74e521fe04
4 changed files with 573 additions and 98 deletions
+207
View File
@@ -0,0 +1,207 @@
-- Attribute block toggle - Interactive expand/collapse of {...} blocks
local M = {}
-- Store expanded content per buffer
-- Format: { [bufnr] = { [line_num] = { collapsed = "...", expanded = "actual content", is_collapsed = boolean } } }
local buffer_attr_state = {}
-- Find attribute block under cursor
-- Returns: start_col, end_col, content (or nil if not found)
function M.find_attr_block_under_cursor()
local line = vim.api.nvim_get_current_line()
local col = vim.api.nvim_win_get_cursor(0)[2] + 1 -- 1-indexed
-- Find all {...} blocks in the line
local pos = 1
while pos <= #line do
local start_pos, end_pos = line:find("{(.-)}", pos)
if not start_pos then
break
end
-- Check if cursor is within this block (including braces)
if col >= start_pos and col <= end_pos then
local content = line:sub(start_pos + 1, end_pos - 1)
return start_pos, end_pos, content
end
pos = end_pos + 1
end
return nil, nil, nil
end
-- Toggle the attribute block under cursor
function M.toggle_attr_block()
local bufnr = vim.api.nvim_get_current_buf()
local line_num = vim.api.nvim_win_get_cursor(0)[1]
local line = vim.api.nvim_get_current_line()
local start_col, end_col, content = M.find_attr_block_under_cursor()
if not start_col then
vim.notify("No attribute block found under cursor", vim.log.levels.WARN)
return
end
-- Initialize buffer state if needed
if not buffer_attr_state[bufnr] then
buffer_attr_state[bufnr] = {}
end
local state_key = line_num .. "_" .. start_col
local state = buffer_attr_state[bufnr][state_key]
-- Check if this is currently collapsed
local is_collapsed = content == "..."
if is_collapsed then
-- Expand: restore original content
if state and state.expanded then
local new_line = line:sub(1, start_col) .. state.expanded .. line:sub(end_col)
vim.api.nvim_set_current_line(new_line)
buffer_attr_state[bufnr][state_key].is_collapsed = false
vim.notify("Attribute block expanded", vim.log.levels.INFO)
else
vim.notify("Cannot expand: original content not stored", vim.log.levels.WARN)
end
else
-- Collapse: store original and show {...}
local new_line = line:sub(1, start_col) .. "..." .. line:sub(end_col)
vim.api.nvim_set_current_line(new_line)
buffer_attr_state[bufnr][state_key] = {
collapsed = "...",
expanded = content,
is_collapsed = true,
line_num = line_num,
start_col = start_col,
}
vim.notify("Attribute block collapsed", vim.log.levels.INFO)
end
end
-- Expand all collapsed blocks in current buffer
function M.expand_all_attr_blocks()
local bufnr = vim.api.nvim_get_current_buf()
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local count = 0
if not buffer_attr_state[bufnr] then
vim.notify("No collapsed blocks to expand", vim.log.levels.INFO)
return
end
for state_key, state in pairs(buffer_attr_state[bufnr]) do
if state.is_collapsed then
-- state.line_num is 1-indexed, but nvim_buf_get_lines returns 0-indexed array
local line = lines[state.line_num]
if line then
-- Find the collapsed {...} and replace with expanded content
local new_line = line:sub(1, state.start_col) .. state.expanded .. line:sub(state.start_col + 4)
lines[state.line_num] = new_line
state.is_collapsed = false
count = count + 1
end
end
end
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
vim.notify("Expanded " .. count .. " attribute block(s)", vim.log.levels.INFO)
end
-- Collapse all attribute blocks matching patterns in current buffer
function M.collapse_all_attr_blocks(patterns)
patterns = patterns or {
"^%s*EXTERNAL", -- Match EXTERNALACCESSIBLE, EXTERNALVISIBLE, etc. (with optional leading space)
"^%s*S7_", -- Match S7_Optimized_Access, etc. (with optional leading space)
}
local bufnr = vim.api.nvim_get_current_buf()
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local count = 0
-- Initialize buffer state
if not buffer_attr_state[bufnr] then
buffer_attr_state[bufnr] = {}
end
for line_idx, line in ipairs(lines) do
local pos = 1
while pos <= #line do
local start_pos, end_pos, capture = line:find("{(.-)}", pos)
if not start_pos then
break
end
local content = line:sub(start_pos + 1, end_pos - 1)
-- Check if content matches any pattern
local should_collapse = false
for _, pattern in ipairs(patterns) do
if content:match(pattern) then
should_collapse = true
break
end
end
-- Don't collapse if already collapsed
if should_collapse and content ~= "..." then
local state_key = line_idx .. "_" .. start_pos
buffer_attr_state[bufnr][state_key] = {
collapsed = "...",
expanded = content,
is_collapsed = true,
line_num = line_idx,
start_col = start_pos,
}
-- Replace in line
line = line:sub(1, start_pos) .. "..." .. line:sub(end_pos)
count = count + 1
-- Update position for next search
pos = start_pos + 4
else
pos = end_pos + 1
end
end
lines[line_idx] = line
end
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
vim.notify("Collapsed " .. count .. " attribute block(s)", vim.log.levels.INFO)
end
-- Clear state when buffer is unloaded
function M.clear_buffer_state(bufnr)
buffer_attr_state[bufnr] = nil
end
-- Setup autocmd to clear state on buffer unload
function M.setup()
local augroup = vim.api.nvim_create_augroup("SCLAttrToggle", { clear = true })
vim.api.nvim_create_autocmd("BufUnload", {
group = augroup,
pattern = "*.scl",
callback = function(args)
M.clear_buffer_state(args.buf)
end,
})
-- Create commands
vim.api.nvim_create_user_command("SCLToggleAttrBlock", function()
M.toggle_attr_block()
end, {})
vim.api.nvim_create_user_command("SCLExpandAllAttrBlocks", function()
M.expand_all_attr_blocks()
end, {})
vim.api.nvim_create_user_command("SCLCollapseAllAttrBlocks", function()
M.collapse_all_attr_blocks()
end, {})
end
return M
+73
View File
@@ -88,6 +88,9 @@ function M.setup(opts)
-- Multiline parameters feature
M.setup_multiline_params()
-- Attribute block toggle feature
M.setup_attr_toggle()
end
function M.setup_multiline_params()
@@ -171,6 +174,76 @@ function M.create_commands()
vim.api.nvim_create_user_command("SCLShowVariables", function() M.show_variables() end, {})
vim.api.nvim_create_user_command("SCLShowWorkspaceTypes", function() M.show_workspace_types() end, {})
vim.api.nvim_create_user_command("SCLRescanWorkspaceTypes", function() M.rescan_workspace_types() end, {})
-- Attribute toggle commands
vim.api.nvim_create_user_command("SCLToggleAttrBlock", function()
local ok, at = pcall(require, "scl.attr_toggle")
if ok then
at.toggle_attr_block()
else
vim.notify("SCL: Failed to load attr_toggle module", vim.log.levels.ERROR)
end
end, {})
vim.api.nvim_create_user_command("SCLExpandAllAttrBlocks", function()
local ok, at = pcall(require, "scl.attr_toggle")
if ok then
at.expand_all_attr_blocks()
else
vim.notify("SCL: Failed to load attr_toggle module", vim.log.levels.ERROR)
end
end, {})
vim.api.nvim_create_user_command("SCLCollapseAllAttrBlocks", function()
local ok, at = pcall(require, "scl.attr_toggle")
if ok then
at.collapse_all_attr_blocks()
else
vim.notify("SCL: Failed to load attr_toggle module", vim.log.levels.ERROR)
end
end, {})
end
function M.setup_attr_toggle()
local at = require("scl.attr_toggle")
at.setup()
local function setup_buffer_keymaps(bufnr)
local opts = { buffer = bufnr, silent = true }
local ft = vim.bo[bufnr].filetype
if ft ~= "scl" and ft ~= "udt" then
return
end
-- Toggle individual attribute block under cursor
vim.keymap.set({ "n", "i" }, "<Leader>xa", function()
at.toggle_attr_block()
end, vim.tbl_extend("force", opts, { desc = "SCL: Toggle attribute block" }))
-- Expand all collapsed blocks
vim.keymap.set("n", "<Leader>xae", function()
at.expand_all_attr_blocks()
end, vim.tbl_extend("force", opts, { desc = "SCL: Expand all attribute blocks" }))
-- Collapse all attribute blocks
vim.keymap.set("n", "<Leader>xac", function()
at.collapse_all_attr_blocks()
end, vim.tbl_extend("force", opts, { desc = "SCL: Collapse all attribute blocks" }))
end
-- Set up keybindings for SCL files only
vim.api.nvim_create_autocmd("FileType", {
pattern = { "scl", "udt" },
callback = function(args)
setup_buffer_keymaps(args.buf)
end,
})
-- Also check if there are already SCL buffers open
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
if vim.api.nvim_buf_is_loaded(bufnr) then
setup_buffer_keymaps(bufnr)
end
end
end
return M