# 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 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 ```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 ``` ## 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: ```lua -- Before formatting: statCntrPartsIn{EXTERNALACCESSIBLE := 'false'; EXTERNALVISIBLE := 'false'} : Int; -- After formatting: statCntrPartsIn{...} : Int; ``` ### Formatter Configuration ```lua 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): ```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 | ## 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" ```