# 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 ```bash make start # Start LSP server make test # Run LSP in test mode lua src/main.lua --test # Direct test mode execution ``` ### Tree-sitter Parser ```bash tree-sitter generate # Generate parser from grammar.js tree-sitter test # Run tree-sitter tests tree-sitter parse # 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: ```lua -- 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 function` before public functions - Boolean variables: 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 ### 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 result - `parse_*_content(content, filename)` - Parse string content - `get_*(name)` - Get single cached item by name - `get_all_*_names()` - Return list of all cached names - `get_*_members(name)` - Get members/fields for a type - `is_*_type(name)` - Check if name is a known type - `clear_cache()` - Clear all cached data - `get_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: ```lua function M.clear_cache() for k in pairs(cache) do cache[k] = nil end end ``` ## Error Handling ### Return Pattern Functions that can fail return `nil, "error message"`: ```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 Always wrap external requires in `pcall()`: ```lua 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: ```lua 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: ```scl "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/ |