# AGENTS.md - tia-lsp A Neovim/LazyVim plugin and standalone LSP server for Siemens TIA Portal languages. Currently supports **SCL**; planned support for STL/AWL, LAD, FBD. The project is split into two repos: - **`tia-lsp`** (this repo) — LSP server (`src/`) + tree-sitter grammar (`grammar.js`). Editor-agnostic. - **`tia-lsp.nvim`** — Neovim plugin (`lua/`) that launches the server and adds editor features. ## 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 all parser tests tree-sitter parse # Parse single SCL file # Lua Linting luacheck src/ # Lint LSP server code luacheck lua/tia/ # Lint plugin modules # Unit & Integration Tests lua test/run_tests.lua # Run all tests lua test/test_udt.lua # Run UDT parser tests lua test/test_db.lua # Run DB parser tests lua test/test_fb.lua # Run FB parser tests lua test/test_formatter.lua # Run formatter tests lua test/test_plc_json.lua # Run plc_json tests lua test/test_integration.lua # Run integration tests # Reference Project # Tests use files from: ~/dev/siemens/projects/scl_lang_support_lazyvim_ref_project/ # Override with: SCL_REF_PROJECT=/path/to/ref lua tests/run_tests.lua ``` ## Project Structure ``` tia-lsp/ ├── src/ # LSP server (uses dofile) │ ├── main.lua # Entry point, JSON-RPC protocol │ ├── parser.lua # SCL parser (will become parser_scl.lua + dispatch) │ ├── diagnostics.lua # Linter (source = "tia_lsp") │ ├── formatter.lua # Document formatter │ ├── treesitter.lua # Tree-sitter integration │ ├── plc_json.lua # External UDT/DB loading (language-agnostic) │ ├── builtin_instructions.lua # TIA Portal built-in types │ └── json.lua # JSON encoder/decoder ├── lua/tia/ # Neovim plugin (uses require) │ ├── 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 ├── lua/tia_lsp/ # Plugin entry point │ ├── init.lua # Main plugin setup (vim.lsp.start) │ └── plugins/scl.lua # LazyVim plugin spec ├── queries/ # Tree-sitter queries (per-language: queries/scl/) └── grammar.js # Tree-sitter grammar (language name: 'scl') ``` ## Naming Convention - **Project name** (repo, Lua module, LSP server, mason package): `tia-lsp` / `tia_lsp` — umbrella for all TIA languages. - **Language name** (filetype, tree-sitter parser, grammar): `scl` — stays as-is. Future languages (`stl`, `lad`, `fbd`) get their own names. - **LSP client name**: `tia_lsp` (one client handles all TIA languages; dispatches by `filetype` / `languageId`). - **Diagnostic source**: `tia_lsp`. - **Commands**: `:SCLFormat`, `:SCLShowVariables`, etc. — per-filetype prefix. Future: `:STLFormat`, etc. ## Filetype Detection After the split, `ftdetect/scl.vim` will live in `tia-lsp.nvim`. Currently required in Neovim config: ```vim au BufRead,BufNewFile *.scl set filetype=scl au BufRead,BufNewFile *.udt set filetype=scl au BufRead,BufNewFile *.db set filetype=scl ``` ## Code Style Guidelines ### Module Pattern ```lua local M = {} 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/tia/` modules: `require("tia.module_name")` - `lua/tia_lsp/` entry: `require("tia_lsp")` - `src/` modules: `dofile(script_path .. "/module.lua")` - External dependencies: wrap in `pcall()` for safety ### Code Formatting - `lua/tia/` and `lua/tia_lsp/`: 2 spaces indentation - `src/`: tab-based indentation - Max line length: 120 characters - No trailing whitespace ### Comments Add comments to explain non-obvious code sections, complex logic, and public API functions. Keep comments concise and relevant. ### 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 ``` ### Error Handling ```lua function M.parse_file(filepath) local file = io.open(filepath, "r") if not file then return nil, "Cannot open file: " .. filepath end return result end -- External dependencies local ok, module = pcall(require, "some_module") if ok and module then module.do_something() end ``` ## Lua Reserved Keywords Cannot use reserved keywords as table keys directly: ```lua -- WRONG local range = { start = pos1, end = pos2 } -- CORRECT local range = { start = pos1, ["end"] = pos2 } ``` Critical for LSP range objects with `end` field. ## Type System The LSP validates data types and provides warnings for unknown types: - **Elementary**: BOOL, BYTE, WORD, DWORD, INT, DINT, REAL, TIME, STRING, etc. - **Timer/counter**: IEC_TIMER, TON_TIME, TOF_TIME, CTU, CTD, CTUD - **Built-in FBs**: TON, TOF, TP, R_TRIG, F_TRIG, etc. - **Built-in functions**: ADD, SUB, MUL, DIV, SIN, COS, SQRT, etc. - **Workspace types**: UDTs from `.udt` files, DBs from `.db` files ## Testing Use the reference project for testing: ``` ~/dev/siemens/projects/scl_lang_support_lazyvim_ref_project/ ``` Contains `.scl`, `.db`, `.udt`, and `.xml` files for comprehensive testing of: - LSP features (hover, completion, go-to-definition) - Formatter (`:SCLFormat`) - Attribute block toggle - Workspace type scanning