9.5 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 = 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
^%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.
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"