Merge branch 'feat/scl_formatter' into develop

This commit is contained in:
2026-02-23 10:30:32 +01:00
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
+5 -4
View File
@@ -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 `<Leader>xae` to expand all blocks in buffer
- Use `<Leader>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
+25 -3
View File
@@ -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
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)
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" },
+2 -4
View File
@@ -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())