# 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 ```bash # 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 # 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`): ```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() 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 function` before public functions - 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")` - External dependencies: wrap in `pcall()` for safety ### Code Formatting - `lua/scl/`: 2 spaces indentation - `src/`: tab-based indentation - Max line length: 120 characters - No trailing whitespace ### Parser Module API All parser modules must implement: - `parse_*_file(filepath)` - Parse and cache - `parse_*_content(content, filename)` - Parse string - `get_*(name)` - Get cached item - `get_all_*_names()` - List all cached names - `get_*_members(name)` - Get members/fields - `is_*_type(name)` - Check if known type - `clear_cache()` - Clear cached data - `get_cache_count()` - Return cache size ### Cache Management ```lua 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: ```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 ```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 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 -- Before formatting: statCntrPartsIn{EXTERNALACCESSIBLE := 'false'; EXTERNALVISIBLE := 'false'} : Int; -- After formatting: statCntrPartsIn{...} : Int; ``` ### 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", }, } ``` **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 ### 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). ## Keybindings (SCL/UDT files) ### Attribute Block Toggle Interactive expand/collapse of individual `{...}` blocks (uses `x` prefix): | Key | Mode | Description | |-----|------|-------------| | `xa` | Normal/Insert | Toggle block under cursor | | `xae` | Normal | Expand all collapsed blocks | | `xac` | Normal | Collapse all attribute blocks | **How it works:** 1. Place cursor inside any `{...}` block 2. Press `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 | |-----|------|-------------| | `xa` | Normal/Insert | Toggle attribute block under cursor | | `xae` | Normal | Expand all attribute blocks | | `xac` | Normal | Collapse all attribute blocks | | `mp` | Insert | Fill multiline FB parameters | | `` | Insert | Jump to next parameter or regular Tab | Note: `` is typically `\` (backslash) or `` 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 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. ## 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] ``` ### 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" ```