diff --git a/AGENTS.md b/AGENTS.md index 4c08939..601dac9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,59 +1,63 @@ -# AGENTS.md - SCL Language Server (Unified) +# AGENTS.md - SCL Language Server A unified Neovim/LazyVim plugin for Siemens SCL language support providing LSP, linting, formatting, syntax highlighting, and auto-completion. -## Reference Project for Testing - -For manual testing and SCL/UDT/DB file examples, use: -``` -~/dev/siemens/projects/scl_lang_support_lazyvim_ref_project -``` - ## Build/Lint/Test Commands -### LSP Server ```bash +# LSP Server make start # Start LSP server make test # Run LSP in test mode -lua src/main.lua --test # Direct test mode execution -``` +lua src/main.lua --test # Direct test execution -### Tree-sitter Parser -```bash +# Tree-sitter Parser tree-sitter generate # Generate parser from grammar.js -tree-sitter test # Run tree-sitter tests -tree-sitter parse # Parse a single SCL file +tree-sitter test # Run parser tests +tree-sitter parse # Parse single SCL file + +# Lua Linting +luacheck src/ # Lint LSP server code +luacheck lua/scl/ # Lint plugin modules ``` ## Project Structure ``` scl_lsp/ -├── src/ # Standalone LSP server (uses dofile) -│ ├── main.lua # Entry point, JSON-RPC protocol -│ ├── parser.lua # Regex-based SCL parser -│ ├── diagnostics.lua # Linter diagnostics -│ └── formatter.lua # Document formatter -├── lua/scl/ # Neovim plugin modules (uses require) -│ ├── blink_cmp_source.lua # blink.cmp completion source -│ ├── udt_parser.lua # UDT (.udt) parser -│ ├── db_parser.lua # Global DB (.db) parser -│ ├── fb_parser.lua # Function Block parser -│ ├── variables.lua # Local variable extraction -│ ├── workspace_types.lua # Workspace scanning -│ └── multiline_params.lua # FB parameter filling -└── queries/ # Tree-sitter queries +├── src/ # LSP server (uses dofile) +│ ├── main.lua # Entry point, JSON-RPC protocol +│ ├── parser.lua # SCL parser +│ ├── diagnostics.lua # Linter +│ ├── formatter.lua # Document formatter +│ ├── treesitter.lua # Tree-sitter integration +│ ├── plc_json.lua # External UDT loading +│ └── json.lua # JSON encoder/decoder +├── lua/scl/ # Neovim plugin (uses require) +│ ├── init.lua # Main plugin setup +│ ├── blink_cmp_source.lua +│ ├── udt_parser.lua # UDT parser +│ ├── db_parser.lua # Global DB parser +│ ├── fb_parser.lua # Function Block parser +│ ├── variables.lua # Variable extraction +│ ├── workspace_types.lua # Workspace scanning +│ ├── multiline_params.lua +│ ├── auto_prefix.lua +│ └── attr_toggle.lua # Interactive attribute block toggle +├── queries/ # Tree-sitter queries +│ ├── highlights.scm +│ ├── indents.scm +│ └── folds.scm +└── grammar.js # Tree-sitter grammar ``` ## Code Style Guidelines ### Module Pattern -All modules follow the standard Lua module pattern: ```lua --- Module description comment at top +-- File header comment local M = {} --- Private module-level cache +-- Private cache local cache = {} function M.public_function() @@ -66,37 +70,37 @@ return M ``` ### Naming Conventions -- Functions/variables: `snake_case` (e.g., `get_udt_members`, `var_types`) +- Functions/variables: `snake_case` (e.g., `get_udt_members`) - Constants: `UPPER_SNAKE_CASE` (e.g., `PROJECT_MARKERS`) - Private functions: declare as `local function` before public functions -- Boolean variables: prefix with `is_`, `has_` (e.g., `is_udt`, `has_members`) +- Booleans: prefix with `is_`, `has_` (e.g., `is_udt`, `has_members`) ### Imports - `lua/scl/` modules: use `require("scl.module_name")` - `src/` modules: use `dofile(script_path .. "/module.lua")` - External dependencies: wrap in `pcall()` for safety -### Comments -- Every file must have a header comment explaining its purpose -- All functions should have a brief comment describing what they do -- Complex logic requires inline comments explaining the "why" -- Non-obvious regex patterns must have explanatory comments +### Code Formatting +- `lua/scl/`: 2 spaces indentation +- `src/`: tab-based indentation +- Max line length: 120 characters +- No trailing whitespace -### Parser Module API Convention -All parser modules (udt_parser, db_parser, fb_parser) must implement: -- `parse_*_file(filepath)` - Parse a file and cache result -- `parse_*_content(content, filename)` - Parse string content -- `get_*(name)` - Get single cached item by name -- `get_all_*_names()` - Return list of all cached names -- `get_*_members(name)` - Get members/fields for a type -- `is_*_type(name)` - Check if name is a known type -- `clear_cache()` - Clear all cached data -- `get_cache_count()` - Return number of cached items +### Parser Module API +All parser modules must implement: +- `parse_*_file(filepath)` - Parse and cache +- `parse_*_content(content, filename)` - Parse string +- `get_*(name)` - Get cached item +- `get_all_*_names()` - List all cached names +- `get_*_members(name)` - Get members/fields +- `is_*_type(name)` - Check if known type +- `clear_cache()` - Clear cached data +- `get_cache_count()` - Return cache size ### Cache Management -- Use module-level local tables for caching: `local cache = {}` -- Clear caches by iterating and setting to nil, not by reassigning: ```lua +local cache = {} + function M.clear_cache() for k in pairs(cache) do cache[k] = nil @@ -106,21 +110,20 @@ end ## Lua Reserved Keywords -Lua reserved keywords (like `end`, `for`, `in`, etc.) cannot be used as table keys directly. Use bracket notation: +Cannot use reserved keywords as table keys directly: ```lua --- WRONG: causes syntax error +-- WRONG: syntax error local range = { start = pos1, end = pos2 } --- CORRECT: use bracket notation +-- CORRECT: bracket notation local range = { start = pos1, ["end"] = pos2 } ``` -This is especially important for LSP range objects which require an `end` field per the LSP specification. +Critical for LSP range objects with `end` field. ## Error Handling ### Return Pattern -Functions that can fail return `nil, "error message"`: ```lua function M.parse_file(filepath) local file = io.open(filepath, "r") @@ -133,7 +136,6 @@ end ``` ### External Dependencies -Always wrap external requires in `pcall()`: ```lua local ok, module = pcall(require, "some_module") if ok and module then @@ -141,72 +143,187 @@ if ok and module then end ``` -### Validation -Check required parameters early and return sensible defaults: -```lua -function M.get_members(type_name) - if not type_name then - return {} - end - -- ... continue -end -``` - -## Completion System - -### Trigger Characters +## Completion Triggers - `#` - Local variables (after BEGIN) -- `.` - Member access (UDT/DB members) +- `.` - Member access (UDT/DB) - `"` - Global DB names - `(` - FB/Function parameters - ` ` (space) - General completion -### Active Portion Detection -When matching patterns in completion, use the "active portion" of the line (after the last operator) to correctly handle expressions like: -```scl -"DB1".member := "DB2".member +## Formatter Options + +The formatter supports collapsing verbose attribute blocks: + +```lua +-- Before formatting: +statCntrPartsIn{EXTERNALACCESSIBLE := 'false'; EXTERNALVISIBLE := 'false'} : Int; + +-- After formatting: +statCntrPartsIn{...} : Int; ``` -Find the last `:=`, `=>`, `=`, `<>`, `>=`, `<=`, `>`, `<`, `AND`, `OR`, `NOT` and only match patterns after it. + +### Formatter Configuration +```lua +local options = { + insertSpaces = false, + tabSize = 1, + collapseAttributes = true, -- Enable attribute collapsing (default: true) + collapsePatterns = { -- Override default patterns + "^%s*{%s*EXTERNAL", + "^%s*{ S7_", + }, + extendCollapsePatterns = { -- Add custom patterns + "^%s*{%s*CUSTOM", + }, +} +``` + +### Default Collapse Patterns +- `^%s*{%s*EXTERNAL` - Variable attributes: `{EXTERNALACCESSIBLE := 'false'; ...}` +- `^%s*{ S7_` - Block attributes: `{ S7_Optimized_Access := 'TRUE' }` +- `^%s*{%s*%w+%s*:=` - Generic attributes with assignments + +### Per-Variable Collapse Rules +Control collapsing per variable using rules (evaluated before global patterns): + +```lua +local options = { + collapseVariableRules = { + -- Collapse attributes for variables starting with "stat" + { + variablePattern = "^stat", + collapse = true, + }, + -- Never collapse for "temp" prefix variables + { + variablePattern = "^temp", + collapse = false, + }, + -- Collapse only if both variable AND attribute match + { + variablePattern = "^config", + attributePattern = "EXTERNAL", + collapse = true, + }, + }, +} +``` + +**Rule properties:** +- `variablePattern` - Lua pattern to match variable name (optional) +- `attributePattern` - Lua pattern to match attribute content (optional) +- `collapse` - `true` to collapse to `{...}`, `false` to keep expanded + +Rules are checked in order; first match wins. ## Commands | Command | Description | |---------|-------------| -| `:SCLShowVariables` | Show local variables in current file | -| `:SCLShowWorkspaceTypes` | Show workspace UDTs, FBs, and Global DBs count | -| `:SCLRescanWorkspaceTypes` | Rescan workspace for types and DBs | -| `:SCLPrefixWord` | Manually prefix current word with `#` | -| `:SCLMultilineParams` | Fill multiline parameters for FB/Function call | -| `:LspSCLFormat` | Format current SCL file | -| `:SCLGeneratePlcJson` | Generate plc.data.json from data_types/ | +| `:SCLShowVariables` | Show local variables | +| `:SCLShowWorkspaceTypes` | Show workspace types count | +| `:SCLRescanWorkspaceTypes` | Rescan workspace | +| `:SCLPrefixWord` | Prefix word with `#` | +| `:SCLMultilineParams` | Fill FB parameters | +| `:LspSCLFormat` | Format SCL file | +| `:SCLToggleAttrBlock` | Toggle attribute block under cursor | +| `:SCLExpandAllAttrBlocks` | Expand all `{...}` blocks in buffer | +| `:SCLCollapseAllAttrBlocks` | Collapse all matching attribute blocks | -## LSP Server Implementation Notes +## Keybindings (SCL/UDT files) + +### Attribute Block Toggle +Interactive expand/collapse of individual `{...}` blocks (uses `x` prefix): + +| Key | Mode | Description | +|-----|------|-------------| +| `xa` | Normal/Insert | Toggle block under cursor | +| `xae` | Normal | Expand all collapsed blocks | +| `xac` | Normal | Collapse all attribute blocks | + +**How it works:** +1. Place cursor inside any `{...}` block +2. Press `xa` to toggle between collapsed `{...}` and expanded content +3. Original content is stored per-buffer and persists until buffer is closed +4. Works on both formatter-collapsed blocks and manually collapsed ones + +### Other Keybindings + +| Key | Mode | Description | +|-----|------|-------------| +| `xa` | Normal/Insert | Toggle attribute block under cursor | +| `xae` | Normal | Expand all attribute blocks | +| `xac` | Normal | Collapse all attribute blocks | +| `mp` | Insert | Fill multiline FB parameters | +| `` | Insert | Jump to next parameter or regular Tab | + +Note: `` is typically `\` (backslash) or `` depending on your configuration. + +### Troubleshooting + +**If commands or keybindings don't work:** + +1. Check if setup() was called: + ```lua + :lua print(vim.inspect(require("scl_lsp"))) + ``` + +2. Verify commands exist: + ```vim + :command SCLToggleAttrBlock + ``` + Should show the command definition. + +3. Check for errors during setup: + ```lua + :lua require("scl_lsp").setup({}) + ``` + +4. Verify leader key: + ```vim + :echo mapleader + ``` + If empty, your leader is `\` (backslash). + +5. Manual test keybinding: + ```vim + :nmap xa + ``` + Should show the mapping. + +## Testing + +Manual testing using: `~/dev/siemens/projects/scl_lang_support_lazyvim_ref_project` + +Workflow: +1. Open `.scl` file in Neovim +2. Test LSP features (hover, completion, go-to-definition) +3. Verify diagnostics +4. Test formatting +5. Check workspace scanning + +## LSP Implementation Notes ### Method Name Conversion -LSP methods like `textDocument/didOpen` are converted to handler names like `textDocument_didOpen`: ```lua local method_name = message.method:gsub("/", "_") local handler = handlers[method_name] ``` ### Request vs Notification -- Requests have `id` field and require a response -- Notifications have no `id` and should not receive a response ```lua if message.id then - -- Only send response for requests + -- Request: send response return { id = message.id, result = result } end +-- Notification: no response needed ``` -### Line Ending Handling -The LSP server strips CRLF (`\r\n`) line endings for Windows compatibility: +### Script Path Pattern ```lua -line = line:gsub("\r$", "") +local script_path = debug.getinfo(1, "S").source:gsub("^@", ""):match("(.*/)") or "" +if script_path == "" then + script_path = "." +end +package.path = package.path .. ";" .. script_path .. "/?.lua" ``` - -### JSON Parser -The standalone JSON parser in `src/json.lua` handles: -- Objects, arrays, strings, numbers, booleans, null -- Escape sequences in strings -- Whitespace skipping diff --git a/lua/scl/attr_toggle.lua b/lua/scl/attr_toggle.lua new file mode 100644 index 0000000..cb4f12f --- /dev/null +++ b/lua/scl/attr_toggle.lua @@ -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 diff --git a/lua/scl/init.lua b/lua/scl/init.lua index d24dac9..c64e446 100644 --- a/lua/scl/init.lua +++ b/lua/scl/init.lua @@ -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" }, "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", "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", "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 diff --git a/lua/scl_lsp/init.lua b/lua/scl_lsp/init.lua index dfe1f46..390ff32 100644 --- a/lua/scl_lsp/init.lua +++ b/lua/scl_lsp/init.lua @@ -64,6 +64,7 @@ function M.setup(opts) M.setup_syntax(opts) M.setup_multiline_params() + M.setup_attr_toggle() M.create_commands() if opts.auto_prefix ~= false then @@ -237,6 +238,83 @@ function M.create_commands() 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, {}) vim.api.nvim_create_user_command("SCLPrefixWord", function() M.prefix_current_word() 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: " .. tostring(at), 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: " .. tostring(at), 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: " .. tostring(at), vim.log.levels.ERROR) + end + end, {}) +end + +function M.setup_attr_toggle() + local ok, at = pcall(require, "scl.attr_toggle") + if not ok then + vim.notify("SCL: attr_toggle module not found", vim.log.levels.WARN) + return + end + + 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" }, "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", "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", "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