diff --git a/AGENTS.md b/AGENTS.md index 90141f4..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 @@ -104,10 +108,22 @@ function M.clear_cache() end ``` +## Lua Reserved Keywords + +Cannot use reserved keywords as table keys directly: +```lua +-- WRONG: syntax error +local range = { start = pos1, end = pos2 } + +-- CORRECT: bracket notation +local range = { start = pos1, ["end"] = pos2 } +``` + +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") @@ -120,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 @@ -128,41 +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 | + +## 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 +```lua +local method_name = message.method:gsub("/", "_") +local handler = handlers[method_name] +``` + +### Request vs Notification +```lua +if message.id then + -- Request: send response + return { id = message.id, result = result } +end +-- Notification: no response needed +``` + +### Script Path Pattern +```lua +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" +``` diff --git a/README.md b/README.md index bc25a38..77f7210 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ A comprehensive Neovim/LazyVim plugin providing **Language Server Protocol (LSP) | **Workspace UDT Scanner** | Scans project for user-defined types (.udt files) | | **Workspace Global DB Scanner** | Scans project for global data blocks (.db files) | | **blink.cmp Integration** | Smart completion source for blink.cmp | +| **Attribute Block Toggle** | Interactive expand/collapse of `{...}` blocks with `xa` | ## Installation @@ -116,6 +117,9 @@ require("scl_lsp").setup({ | `:SCLMultilineParams` | Fill multiline parameters for FB/Function call | | `:LspSCLFormat` | Format current SCL file | | `:SCLGeneratePlcJson` | Generate plc.data.json from data_types/ | +| `:SCLToggleAttrBlock` | Toggle attribute block under cursor | +| `:SCLExpandAllAttrBlocks` | Expand all `{...}` blocks in buffer | +| `:SCLCollapseAllAttrBlocks` | Collapse all matching attribute blocks | ## Keybindings @@ -127,9 +131,32 @@ require("scl_lsp").setup({ | `w` | Workspace Symbols | | `mp` | Fill multiline FB/Function parameters (insert mode) | | `` | Jump to next parameter (insert mode) | +| `xa` | Toggle attribute block under cursor | +| `xae` | Expand all attribute blocks | +| `xac` | Collapse all attribute blocks | | `fw` | Workspace Symbols (Telescope) | | `ca` | Code Actions | +## Attribute Block Toggle + +Interactive expand/collapse of SCL variable attribute blocks: + +```scl +-- Before: Verbose attributes +statCounter{EXTERNALACCESSIBLE := 'false'; EXTERNALVISIBLE := 'false'} : Int; + +-- After: Collapsed +statCounter{...} : Int; +``` + +**Usage:** +- Place cursor inside any `{...}` block +- Press `xa` to toggle expand/collapse +- Use `xae` to expand all blocks in buffer +- Use `xac` to collapse all blocks + +The formatter also supports automatic collapsing via `collapseAttributes` option. + ## External UDT Support ### Option 1: Create `.plc.json` @@ -253,10 +280,24 @@ myVar.temperature := 25.5; END_FUNCTION_BLOCK ``` +## Architecture + +The plugin uses a standalone LSP server (`src/main.lua`) that communicates via JSON-RPC over stdio. The server handles: +- Text document synchronization +- Diagnostics (linting) +- Formatting +- Completion, hover, go-to-definition +- Semantic tokens + +The Neovim plugin (`lua/scl_lsp/init.lua`) manages: +- LSP client lifecycle via `vim.lsp.start()` +- FileType autocmd for lazy loading +- blink.cmp integration +- Workspace scanning for UDTs/DBs + ## Requirements -- **Neovim 0.9+** -- **nvim-lspconfig** - LSP client +- **Neovim 0.11+** (uses `vim.lsp.start()`) - **nvim-treesitter** - Syntax highlighting - **blink.cmp** - Optional, for completion - **Lua 5.1+** - LSP server runtime 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 e823a6c..390ff32 100644 --- a/lua/scl_lsp/init.lua +++ b/lua/scl_lsp/init.lua @@ -13,54 +13,58 @@ function M.setup(opts) setup_package_path() opts = opts or {} - local lspconfig_avail, lspconfig = pcall(require, "lspconfig") - if not lspconfig_avail then - vim.notify("lspconfig not found. Please install nvim-lspconfig.", vim.log.levels.ERROR) - return - end - local server_path = opts.server_path or vim.fn.expand("~/dev/scl_lsp/src/main.lua") - + local root_patterns = { ".git", "data_types", "plc.data.json" } - local function get_root_dir(fname) - local root = vim.fs.root(fname, root_patterns) - return root or vim.fn.fnamemodify(fname, ":h") - end - - local function on_attach(client, bufnr) - if opts.lsp and opts.lsp.on_attach then - opts.lsp.on_attach(client, bufnr) - end - - if opts.lsp.formatting then - vim.api.nvim_buf_create_user_command(bufnr, "LspSCLFormat", function() - vim.lsp.buf.format({ name = "scl-language-server" }) - end, {}) - end - end - - lspconfig.scl_lsp = { - default_config = { - name = "scl-language-server", + + local function start_lsp(bufnr) + local fname = vim.api.nvim_buf_get_name(bufnr) + if fname == "" then return end + + local root_dir = vim.fs.root(fname, root_patterns) + or vim.fn.fnamemodify(fname, ":p:h") + + vim.lsp.start({ + name = "scl_lsp", cmd = { "lua", server_path }, - filetypes = { "scl" }, - root_dir = get_root_dir, - settings = {}, - on_attach = on_attach, - }, - docs = { - description = "Language server for Siemens SCL (LSP, Linter, Formatter)", - default_config = { - name = "scl-language-server", - cmd = { "lua", server_path }, - filetypes = { "scl" }, - root_dir = "root_pattern('.git', 'data_types', 'plc.data.json')", - }, - }, - } + root_dir = root_dir, + on_attach = function(client, b) + if opts.lsp and opts.lsp.on_attach then + opts.lsp.on_attach(client, b) + end + + if opts.lsp and opts.lsp.formatting then + vim.api.nvim_buf_create_user_command(b, "LspSCLFormat", function() + vim.lsp.buf.format({ name = "scl_lsp" }) + end, {}) + end + end, + }) + end + + -- Create FileType autocmd for future buffers + vim.api.nvim_create_autocmd("FileType", { + pattern = "scl", + callback = function(args) + start_lsp(args.buf) + end, + }) + + -- Create global format command (works on current buffer) + vim.api.nvim_create_user_command("LspSCLFormat", function() + vim.lsp.buf.format({ name = "scl_lsp" }) + end, {}) + + -- Start LSP for any existing scl buffers (handles lazy loading case) + for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do + if vim.bo[bufnr].filetype == "scl" and vim.api.nvim_buf_is_loaded(bufnr) then + start_lsp(bufnr) + end + end M.setup_syntax(opts) M.setup_multiline_params() + M.setup_attr_toggle() M.create_commands() if opts.auto_prefix ~= false then @@ -234,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