feat: improve formatter and attribute block handling

- Format multi-line assignments with proper alignment
- Format FB calls in multiline style with parameter alignment
- Auto-expand collapsed attribute blocks before save
- Rename command from LspSCLFormat to SCLFormat
- Condense AGENTS.md and add testing section with reference project
This commit is contained in:
2026-02-23 10:29:05 +01:00
parent 15f88d110b
commit 3dfca6bba3
4 changed files with 68 additions and 290 deletions
+34 -277
View File
@@ -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 <file.scl> # 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 `<Leader>x` prefix):
| Key | Mode | Description |
|-----|------|-------------|
| `<Leader>xa` | Normal/Insert | Toggle block under cursor |
| `<Leader>xae` | Normal | Expand all collapsed blocks |
| `<Leader>xac` | Normal | Collapse all attribute blocks |
**How it works:**
1. Place cursor inside any `{...}` block
2. Press `<Leader>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 |
|-----|------|-------------|
| `<Space>mp` | Insert | Fill multiline FB parameters |
| `<Tab>` | Insert | Jump to next parameter or regular Tab |
Note: `<Leader>` is typically `\` (backslash) or `<Space>` 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 <Leader>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