diff --git a/AGENTS.md b/AGENTS.md index ec7713b..aa98fee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md - SCL Language Server -A unified Neovim/LazyVim plugin for Siemens SCL language support providing LSP, linting, formatting, syntax highlighting, and auto-completion. +A Neovim/LazyVim plugin for Siemens SCL language support providing LSP, linting, formatting, syntax highlighting, and auto-completion. ## Build/Lint/Test Commands @@ -12,7 +12,7 @@ lua src/main.lua --test # Direct test execution # Tree-sitter Parser tree-sitter generate # Generate parser from grammar.js -tree-sitter test # Run parser tests +tree-sitter test # Run all parser tests tree-sitter parse # Parse single SCL file # Lua Linting @@ -31,11 +31,10 @@ scl_lsp/ │ ├── formatter.lua # Document formatter │ ├── treesitter.lua # Tree-sitter integration │ ├── plc_json.lua # External UDT/DB loading -│ ├── builtin_instructions.lua # TIA Portal built-in types/FBs/functions +│ ├── builtin_instructions.lua # TIA Portal built-in types │ └── 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 @@ -43,34 +42,26 @@ scl_lsp/ │ ├── workspace_types.lua # Workspace scanning │ ├── multiline_params.lua │ ├── auto_prefix.lua -│ └── attr_toggle.lua # Interactive attribute block toggle +│ └── attr_toggle.lua ├── queries/ # Tree-sitter queries -│ ├── highlights.scm -│ ├── indents.scm -│ └── folds.scm └── grammar.js # Tree-sitter grammar ``` ## Filetype Detection -The LSP requires the `scl` filetype to be set. Add to your Neovim config (e.g., `ftdetect/scl.vim`): - +Required in Neovim config (ftdetect/scl.vim): ```vim au BufRead,BufNewFile *.scl set filetype=scl au BufRead,BufNewFile *.udt set filetype=scl au BufRead,BufNewFile *.db set filetype=scl ``` -This enables LSP features (completion, goto definition, hover, etc.) for all SCL-related file types. - ## Code Style Guidelines ### Module Pattern ```lua --- File header comment local M = {} --- Private cache local cache = {} function M.public_function() @@ -89,8 +80,8 @@ return M - 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")` +- `lua/scl/` modules: `require("scl.module_name")` +- `src/` modules: `dofile(script_path .. "/module.lua")` - External dependencies: wrap in `pcall()` for safety ### Code Formatting @@ -99,6 +90,9 @@ return M - Max line length: 120 characters - No trailing whitespace +### Comments +**DO NOT ADD COMMENTS** in code unless explicitly requested by the user. + ### Parser Module API All parser modules must implement: - `parse_*_file(filepath)` - Parse and cache @@ -121,291 +115,54 @@ 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 +### Error Handling ```lua function M.parse_file(filepath) local file = io.open(filepath, "r") if not file then return nil, "Cannot open file: " .. filepath end - -- ... parse logic return result end -``` -### External Dependencies -```lua +-- External dependencies local ok, module = pcall(require, "some_module") if ok and module then module.do_something() end ``` -## Diagnostics - -The LSP validates data types and provides warnings for unknown types. It recognizes: -- **Elementary data types** - BOOL, BYTE, WORD, DWORD, LWORD, CHAR, WCHAR, STRING, WSTRING, INT, DINT, LINT, USINT, SINT, UINT, UDINT, ULINT, REAL, LREAL, TIME, DATE, TOD, TIME_OF_DAY, DATE_AND_TIME, DT, S5TIME, LTIME, DTL -- **Timer/counter types** - IEC_TIMER, TON_TIME, TOF_TIME, TONR_TIME, TP_TIME, CTU, CTD, CTUD -- **TIA Portal built-in instructions** - Functions and function blocks -- **Workspace types** - UDTs from `.udt` files and data blocks from `.db` files - -## Completion Triggers -- `#` - Local variables (after BEGIN) -- `.` - Member access (UDT/DB) -- `"` - Global DB names -- `(` - FB/Function parameters -- ` ` (space) - General completion -- `:` - Type declaration (in VAR section) - -### Completion Types -The LSP provides auto-completion for: -- **Local variables** - Variables declared in VAR sections -- **UDT types** - User-defined types from `.udt` files -- **Global DBs** - Data blocks from `.db` files -- **Elementary types** - BOOL, INT, DINT, REAL, TIME, etc. -- **TIA Portal built-in FBs** - TON, TOF, TP, CTU, CTD, CTUD, R_TRIG, etc. -- **TIA Portal built-in functions** - ADD, SUB, MUL, DIV, SIN, COS, SQRT, etc. - -## Formatter Options - -The formatter supports collapsing verbose attribute blocks: +## Lua Reserved Keywords +Cannot use reserved keywords as table keys directly: ```lua --- Before formatting: -statCntrPartsIn{EXTERNALACCESSIBLE := 'false'; EXTERNALVISIBLE := 'false'} : Int; +-- WRONG +local range = { start = pos1, end = pos2 } --- After formatting: -statCntrPartsIn{...} : Int; +-- CORRECT +local range = { start = pos1, ["end"] = pos2 } ``` -### Formatter Configuration -```lua -local options = { - insertSpaces = false, - tabSize = 1, - collapseAttributes = false, -- Disabled by default (see note below) - collapsePatterns = { -- Override default patterns - "^%s*{%s*EXTERNAL", - "^%s*{ S7_", - }, - extendCollapsePatterns = { -- Add custom patterns - "^%s*{%s*CUSTOM", - }, -} -``` +Critical for LSP range objects with `end` field. -**Note:** `collapseAttributes` is disabled by default to preserve compatibility with the interactive toggle feature (`:SCLToggleAttrBlock`). When the formatter automatically collapses blocks, the original content is lost. To use both formatting and toggle: -1. Format with `:LspSCLFormat` (preserves original content) -2. Manually collapse with `:SCLCollapseAllAttrBlocks` (stores original content) -3. Now toggle/expand works correctly +## Type System -### Default Collapse Patterns -The `:SCLCollapseAllAttrBlocks` command uses case-insensitive patterns: -- `EXTERNAL` - Variable attributes: `{EXTERNALACCESSIBLE := 'false'; EXTERNALVISIBLE := 'false'}` -- `S7_` - Block attributes: `{S7_Optimized_Access := 'TRUE'}` -- `NonRetain` - NonRetain attribute: `{NonRetain}` -- `Retain` - Retain attribute: `{Retain}` -- `%w+ :=` - Generic attributes with assignments: `{key := 'value'}` -- `%w+` - Single word attributes: `{SomeAttribute}` - -Works in `.scl`, `.udt`, and `.db` files. - -### 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 | -| `: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 Features - -### Goto Declaration (`gD`) -The LSP supports goto declaration functionality for: -- **Functions** - Jump to function definition in `.scl` files -- **Function Blocks (FB)** - Jump to FB definition in `.scl` files -- **Data Blocks (DB)** - Jump to DB definition in `.scl` and `.db` files -- **User-Defined Types (UDT)** - Jump to type definition in `.scl` and `.udt` files - -Usage: Place cursor on a variable or type name and press `gD` (or use LSP client command). - -## Auto-Editing Features - -### Auto-Uppercase Keywords -When typing SCL keywords followed by a space, they are automatically converted to uppercase: -- `if ` → `IF ` -- `then ` → `THEN ` -- `end_if` → `END_IF` -- etc. - -Works in `.scl`, `.udt`, and `.db` files. - -### Auto-Semicolon for Control Structures -When pressing Enter after an `END_*` control structure keyword, a semicolon is automatically added: -- `END_IF` → `END_IF;` -- `END_FOR` → `END_FOR;` -- `END_WHILE` → `END_WHILE;` -- `END_REPEAT` → `END_REPEAT;` -- `END_CASE` → `END_CASE;` - -Block definitions (END_VAR, END_FUNCTION, etc.) are NOT affected. - -### Auto-Prefix Local Variables -Local variables are automatically prefixed with `#` when typing space or semicolon after BEGIN: -- `myVar := ` → `#myVar := ` -- `counter.in` → `#counter.in` (on dot) - -Can be toggled with `:lua require("scl.auto_prefix").toggle()`. - -## 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 | -|-----|------|-------------| -| `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. - -### Attribute Toggle After Formatting - -**Note:** After using `:LspSCLFormat`, all collapsed attribute blocks will be expanded and the toggle state is reset. This is because formatting changes line numbers and positions. You can collapse blocks again after formatting. +The LSP validates data types and provides warnings for unknown types: +- **Elementary**: BOOL, BYTE, WORD, DWORD, INT, DINT, REAL, TIME, STRING, etc. +- **Timer/counter**: IEC_TIMER, TON_TIME, TOF_TIME, CTU, CTD, CTUD +- **Built-in FBs**: TON, TOF, TP, R_TRIG, F_TRIG, etc. +- **Built-in functions**: ADD, SUB, MUL, DIV, SIN, COS, SQRT, etc. +- **Workspace types**: UDTs from `.udt` files, DBs from `.db` files ## 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] +Use the reference project for testing: +``` +~/dev/siemens/projects/scl_lang_support_lazyvim_ref_project/ ``` -### 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" -``` +Contains `.scl`, `.db`, `.udt`, and `.xml` files for comprehensive testing of: +- LSP features (hover, completion, go-to-definition) +- Formatter (`:SCLFormat`) +- Attribute block toggle +- Workspace type scanning diff --git a/README.md b/README.md index 37236ac..6e177c1 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,9 @@ A comprehensive Neovim/LazyVim plugin providing **Language Server Protocol (LSP) ### Formatter | Feature | Description | |---------|-------------| -| **Document Formatting** | Format entire SCL files | +| **Document Formatting** | Format entire SCL files with `:SCLFormat` | +| **Multi-line Assignments** | Preserves and formats multi-line expressions with proper alignment | +| **FB Call Formatting** | Formats function block calls in multiline style | | **Indentation** | Proper block indentation | | **Keyword Casing** | Maintains SCL keyword casing | @@ -119,8 +121,7 @@ require("scl_lsp").setup({ | `: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/ | +| `:SCLFormat` | Format current SCL file | | `:SCLToggleAttrBlock` | Toggle attribute block under cursor | | `:SCLExpandAllAttrBlocks` | Expand all `{...}` blocks in buffer | | `:SCLCollapseAllAttrBlocks` | Collapse all matching attribute blocks | @@ -159,7 +160,7 @@ statCounter{...} : Int; - Use `xae` to expand all blocks in buffer - Use `xac` to collapse all blocks -**Note:** The formatter has automatic collapsing disabled by default to preserve compatibility with the toggle feature. Format with `:LspSCLFormat` first, then use `:SCLCollapseAllAttrBlocks` to collapse while preserving the ability to expand later. +**Note:** Collapsed blocks are automatically expanded before saving to preserve full content on disk. Use `:SCLFormat` to format, then `:SCLCollapseAllAttrBlocks` to collapse while preserving the ability to expand later. ## External UDT Support diff --git a/lua/scl/attr_toggle.lua b/lua/scl/attr_toggle.lua index d5cc6e0..e65b4fa 100644 --- a/lua/scl/attr_toggle.lua +++ b/lua/scl/attr_toggle.lua @@ -80,23 +80,24 @@ function M.toggle_attr_block() end -- Expand all collapsed blocks in current buffer -function M.expand_all_attr_blocks() +function M.expand_all_attr_blocks(opts) + opts = opts or {} 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) + if not opts.silent then + vim.notify("No collapsed blocks to expand", vim.log.levels.INFO) + end 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 @@ -106,7 +107,9 @@ function M.expand_all_attr_blocks() end vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) - vim.notify("Expanded " .. count .. " attribute block(s)", vim.log.levels.INFO) + if not opts.silent and count > 0 then + vim.notify("Expanded " .. count .. " attribute block(s)", vim.log.levels.INFO) + end end -- Helper to make a pattern case-insensitive @@ -193,6 +196,25 @@ end function M.setup() local augroup = vim.api.nvim_create_augroup("SCLAttrToggle", { clear = true }) + vim.api.nvim_create_autocmd("BufWritePre", { + group = augroup, + pattern = { "*.scl", "*.udt", "*.db" }, + callback = function(args) + if buffer_attr_state[args.buf] then + local has_collapsed = false + for _, state in pairs(buffer_attr_state[args.buf]) do + if state.is_collapsed then + has_collapsed = true + break + end + end + if has_collapsed then + M.expand_all_attr_blocks({ silent = true }) + end + end + end, + }) + vim.api.nvim_create_autocmd("BufUnload", { group = augroup, pattern = { "*.scl", "*.udt", "*.db" }, diff --git a/lua/scl_lsp/init.lua b/lua/scl_lsp/init.lua index 5800d5f..45c413e 100644 --- a/lua/scl_lsp/init.lua +++ b/lua/scl_lsp/init.lua @@ -34,9 +34,8 @@ function M.setup(opts) end if opts.lsp and opts.lsp.formatting then - vim.api.nvim_buf_create_user_command(b, "LspSCLFormat", function() + vim.api.nvim_buf_create_user_command(b, "SCLFormat", function() vim.lsp.buf.format({ name = "scl_lsp" }) - -- Clear attr_toggle state since line positions may have changed local ok, at = pcall(require, "scl.attr_toggle") if ok then at.clear_buffer_state(vim.api.nvim_get_current_buf()) @@ -56,9 +55,8 @@ function M.setup(opts) }) -- Create global format command (works on current buffer) - vim.api.nvim_create_user_command("LspSCLFormat", function() + vim.api.nvim_create_user_command("SCLFormat", function() vim.lsp.buf.format({ name = "scl_lsp" }) - -- Clear attr_toggle state since line positions may have changed local ok, at = pcall(require, "scl.attr_toggle") if ok then at.clear_buffer_state(vim.api.nvim_get_current_buf())