12 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/DB loading
│ ├── builtin_instructions.lua # TIA Portal built-in types/FBs/functions
│ └── 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
Filetype Detection
The LSP requires the scl filetype to be set. Add to your Neovim config (e.g., ftdetect/scl.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
-- 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
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
.udtfiles and data blocks from.dbfiles
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
.udtfiles - Global DBs - Data blocks from
.dbfiles - 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:
-- Before formatting:
statCntrPartsIn{EXTERNALACCESSIBLE := 'false'; EXTERNALVISIBLE := 'false'} : Int;
-- After formatting:
statCntrPartsIn{...} : Int;
Formatter Configuration
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",
},
}
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:
- Format with
:LspSCLFormat(preserves original content) - Manually collapse with
:SCLCollapseAllAttrBlocks(stores original content) - Now toggle/expand works correctly
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):
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 |
LSP Features
Goto Declaration (gD)
The LSP supports goto declaration functionality for:
- Functions - Jump to function definition in
.sclfiles - Function Blocks (FB) - Jump to FB definition in
.sclfiles - Data Blocks (DB) - Jump to DB definition in
.scland.dbfiles - User-Defined Types (UDT) - Jump to type definition in
.scland.udtfiles
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→IFthen→THENend_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:
- 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 |
|---|---|---|
<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.
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.
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"