Add ability to expand/collapse SCL variable attribute blocks like {EXTERNALACCESSIBLE := 'false'} to {...}
Features:
- Per-variable collapse rules via collapseVariableRules option
- Interactive toggle with <Leader>xa keybinding
- Commands: SCLToggleAttrBlock, SCLExpandAllAttrBlocks, SCLCollapseAllAttrBlocks
- Keybindings: <Leader>xa (toggle), <Leader>xae (expand all), <Leader>xac (collapse all)
- Supports both formatter-collapsed and manually collapsed blocks
Also update AGENTS.md with new documentation and troubleshooting guide
8.8 KiB
AGENTS.md - SCL Language Server
A unified Neovim/LazyVim plugin for Siemens SCL language support providing LSP, linting, formatting, syntax highlighting, and auto-completion.
Build/Lint/Test Commands
# LSP Server
make start # Start LSP server
make test # Run LSP in test mode
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 parse <file.scl> # Parse single SCL file
# Lua Linting
luacheck src/ # Lint LSP server code
luacheck lua/scl/ # Lint plugin modules
Project Structure
scl_lsp/
├── 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
-- File header comment
local M = {}
-- Private cache
local cache = {}
function M.public_function()
end
local function private_function()
end
return M
Naming Conventions
- Functions/variables:
snake_case(e.g.,get_udt_members) - Constants:
UPPER_SNAKE_CASE(e.g.,PROJECT_MARKERS) - Private functions: declare as
local functionbefore public functions - Booleans: prefix with
is_,has_(e.g.,is_udt,has_members)
Imports
lua/scl/modules: userequire("scl.module_name")src/modules: usedofile(script_path .. "/module.lua")- External dependencies: wrap in
pcall()for safety
Code Formatting
lua/scl/: 2 spaces indentationsrc/: tab-based indentation- Max line length: 120 characters
- No trailing whitespace
Parser Module API
All parser modules must implement:
parse_*_file(filepath)- Parse and cacheparse_*_content(content, filename)- Parse stringget_*(name)- Get cached itemget_all_*_names()- List all cached namesget_*_members(name)- Get members/fieldsis_*_type(name)- Check if known typeclear_cache()- Clear cached dataget_cache_count()- Return cache size
Cache Management
local cache = {}
function M.clear_cache()
for k in pairs(cache) do
cache[k] = nil
end
end
Lua Reserved Keywords
Cannot use reserved keywords as table keys directly:
-- 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
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
local ok, module = pcall(require, "some_module")
if ok and module then
module.do_something()
end
Completion Triggers
#- Local variables (after BEGIN).- Member access (UDT/DB)"- Global DB names(- FB/Function parameters(space) - General completion
Formatter Options
The formatter supports collapsing verbose attribute blocks:
-- Before formatting:
statCntrPartsIn{EXTERNALACCESSIBLE := 'false'; EXTERNALVISIBLE := 'false'} : Int;
-- After formatting:
statCntrPartsIn{...} : Int;
Formatter Configuration
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):
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-trueto collapse to{...},falseto 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 |
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:
- Place cursor inside any
{...}block - Press
<Leader>xato toggle between collapsed{...}and expanded content - Original content is stored per-buffer and persists until buffer is closed
- Works on both formatter-collapsed blocks and manually collapsed ones
Other Keybindings
| Key | Mode | Description |
|---|---|---|
<Leader>xa |
Normal/Insert | Toggle attribute block under cursor |
<Leader>xae |
Normal | Expand all attribute blocks |
<Leader>xac |
Normal | Collapse all attribute blocks |
<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:
-
Check if setup() was called:
:lua print(vim.inspect(require("scl_lsp"))) -
Verify commands exist:
:command SCLToggleAttrBlockShould show the command definition.
-
Check for errors during setup:
:lua require("scl_lsp").setup({}) -
Verify leader key:
:echo mapleaderIf empty, your leader is
\(backslash). -
Manual test keybinding:
:nmap <Leader>xaShould show the mapping.
Testing
Manual testing using: ~/dev/siemens/projects/scl_lang_support_lazyvim_ref_project
Workflow:
- Open
.sclfile in Neovim - Test LSP features (hover, completion, go-to-definition)
- Verify diagnostics
- Test formatting
- Check workspace scanning
LSP Implementation Notes
Method Name Conversion
local method_name = message.method:gsub("/", "_")
local handler = handlers[method_name]
Request vs Notification
if message.id then
-- Request: send response
return { id = message.id, result = result }
end
-- Notification: no response needed
Script Path Pattern
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"