- Replace lspconfig.scl_lsp registration with vim.lsp.start() + FileType autocmd for Neovim 0.11+ compatibility - Fix Lua reserved keyword 'end' in tables (use bracket notation ['end']) - Fix JSON decoder pattern matching bugs (s:find -> s:sub:match) - Add LSP method name conversion (textDocument/didOpen -> textDocument_didOpen) - Handle request vs notification correctly (only respond to requests with id) - Strip CRLF line endings for Windows compatibility - Add semantic token handler aliases - Update documentation with implementation notes
6.3 KiB
AGENTS.md - SCL Language Server (Unified)
A unified Neovim/LazyVim plugin for Siemens SCL language support providing LSP, linting, formatting, syntax highlighting, and auto-completion.
Reference Project for Testing
For manual testing and SCL/UDT/DB file examples, use:
~/dev/siemens/projects/scl_lang_support_lazyvim_ref_project
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 mode execution
Tree-sitter Parser
tree-sitter generate # Generate parser from grammar.js
tree-sitter test # Run tree-sitter tests
tree-sitter parse <file.scl> # Parse a single SCL file
Project Structure
scl_lsp/
├── src/ # Standalone LSP server (uses dofile)
│ ├── main.lua # Entry point, JSON-RPC protocol
│ ├── parser.lua # Regex-based SCL parser
│ ├── diagnostics.lua # Linter diagnostics
│ └── formatter.lua # Document formatter
├── lua/scl/ # Neovim plugin modules (uses require)
│ ├── blink_cmp_source.lua # blink.cmp completion source
│ ├── udt_parser.lua # UDT (.udt) parser
│ ├── db_parser.lua # Global DB (.db) parser
│ ├── fb_parser.lua # Function Block parser
│ ├── variables.lua # Local variable extraction
│ ├── workspace_types.lua # Workspace scanning
│ └── multiline_params.lua # FB parameter filling
└── queries/ # Tree-sitter queries
Code Style Guidelines
Module Pattern
All modules follow the standard Lua module pattern:
-- Module description comment at top
local M = {}
-- Private module-level 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,var_types) - Constants:
UPPER_SNAKE_CASE(e.g.,PROJECT_MARKERS) - Private functions: declare as
local functionbefore public functions - Boolean variables: 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
Comments
- Every file must have a header comment explaining its purpose
- All functions should have a brief comment describing what they do
- Complex logic requires inline comments explaining the "why"
- Non-obvious regex patterns must have explanatory comments
Parser Module API Convention
All parser modules (udt_parser, db_parser, fb_parser) must implement:
parse_*_file(filepath)- Parse a file and cache resultparse_*_content(content, filename)- Parse string contentget_*(name)- Get single cached item by nameget_all_*_names()- Return list of all cached namesget_*_members(name)- Get members/fields for a typeis_*_type(name)- Check if name is a known typeclear_cache()- Clear all cached dataget_cache_count()- Return number of cached items
Cache Management
- Use module-level local tables for caching:
local cache = {} - Clear caches by iterating and setting to nil, not by reassigning:
function M.clear_cache()
for k in pairs(cache) do
cache[k] = nil
end
end
Lua Reserved Keywords
Lua reserved keywords (like end, for, in, etc.) cannot be used as table keys directly. Use bracket notation:
-- WRONG: causes syntax error
local range = { start = pos1, end = pos2 }
-- CORRECT: use bracket notation
local range = { start = pos1, ["end"] = pos2 }
This is especially important for LSP range objects which require an end field per the LSP specification.
Error Handling
Return Pattern
Functions that can fail return nil, "error message":
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
Always wrap external requires in pcall():
local ok, module = pcall(require, "some_module")
if ok and module then
module.do_something()
end
Validation
Check required parameters early and return sensible defaults:
function M.get_members(type_name)
if not type_name then
return {}
end
-- ... continue
end
Completion System
Trigger Characters
#- Local variables (after BEGIN).- Member access (UDT/DB members)"- Global DB names(- FB/Function parameters(space) - General completion
Active Portion Detection
When matching patterns in completion, use the "active portion" of the line (after the last operator) to correctly handle expressions like:
"DB1".member := "DB2".member
Find the last :=, =>, =, <>, >=, <=, >, <, AND, OR, NOT and only match patterns after it.
Commands
| Command | Description |
|---|---|
:SCLShowVariables |
Show local variables in current file |
:SCLShowWorkspaceTypes |
Show workspace UDTs, FBs, and Global DBs count |
: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/ |
LSP Server Implementation Notes
Method Name Conversion
LSP methods like textDocument/didOpen are converted to handler names like textDocument_didOpen:
local method_name = message.method:gsub("/", "_")
local handler = handlers[method_name]
Request vs Notification
- Requests have
idfield and require a response - Notifications have no
idand should not receive a response
if message.id then
-- Only send response for requests
return { id = message.id, result = result }
end
Line Ending Handling
The LSP server strips CRLF (\r\n) line endings for Windows compatibility:
line = line:gsub("\r$", "")
JSON Parser
The standalone JSON parser in src/json.lua handles:
- Objects, arrays, strings, numbers, booleans, null
- Escape sequences in strings
- Whitespace skipping