# tia-lsp - Language Server for Siemens TIA Portal A Neovim/LazyVim plugin and standalone LSP server providing **Language Server Protocol (LSP)**, **Linting**, **Formatting**, and **Syntax Highlighting** for Siemens TIA Portal languages. Currently supports **SCL** (Structured Control Language), with planned support for **STL/AWL**, **LAD**, and **FBD**. The project is split into two repos: - **`tia-lsp`** (this repo) — the standalone LSP server (`src/`) plus the tree-sitter grammar (`grammar.js`, `src/parser.c`). Speaks JSON-RPC over stdio. Editor-agnostic: usable from Neovim, VS Code, or any LSP client. - **`tia-lsp.nvim`** — the Neovim plugin (`lua/`) that launches the server via `vim.lsp.start()` and adds editor-specific features (auto-prefix, blink.cmp source, workspace UDT scanning, attribute toggle, multiline params). ## Features ### LSP (Language Server Protocol) | Feature | Description | |---------|-------------| | **Hover** | Press `K` to see variable type and struct fields | | **Completion** | `#` for variables, `.` for struct fields, `(` for functions | | **Go to Definition** | `gD` jump to function, FB, DB, or UDT definition | | **Document Symbols** | Shows functions, variables, types | | **Workspace Symbols** | Search across all `.scl` files with fuzzy matching | | **Semantic Tokens** | Keywords, variables highlighted | | **Inlay Hints** | Shows variable types after declaration | | **External UDT** | Load types from `.plc.json` or `data_types/` folder | ### Linter (Diagnostics) | Feature | Description | |---------|-------------| | **Undefined Variables** | Detects variables used without declaration | | **Invalid # Prefix** | Detects `#` prefix in VAR sections | | **Unknown Types** | Warns about unknown data types | | **Type Validation** | Basic type checking | ### Formatter | Feature | Description | |---------|-------------| | **Document Formatting** | Format entire SCL files with `:SCLFormat` | | **Multi-line Assignments** | Preserves and formats multi-line expressions with proper alignment | | **FB Call Formatting** | Formats function block calls in multiline style | | **Indentation** | Proper block indentation | | **Keyword Casing** | Maintains SCL keyword casing | ### Syntax Highlighting - Full SCL syntax support via tree-sitter - Block definitions: ORGANIZATION_BLOCK, FUNCTION_BLOCK, FUNCTION - Variable sections: VAR_INPUT, VAR_OUTPUT, VAR_IN_OUT, VAR_TEMP, VAR_CONSTANT - Control structures: IF/THEN/ELSIF/ELSE/END_IF, CASE/OF/END_CASE, FOR/TO/BY/DO/END_FOR, WHILE/END_WHILE, REPEAT/UNTIL/END_REPEAT - Built-in data types: BOOL, INT, DINT, REAL, STRING, TIME, etc. - Function block instances highlighted in calls - FB parameter names (IN, PT, CU, etc.) highlighted as properties - `#` prefix for local variables highlighted - Operators: AND, OR, NOT, XOR, MOD - Comments: line (`//`), block (`(* *)`), C-style (`/* */`) ### Additional Features | Feature | Description | |---------|-------------| | **Auto-Prefix** | Automatically adds `#` prefix to local variables (like TIA Portal) | | **Multiline Parameters** | Fill FB/Function call parameters across multiple lines with `mp` or `:SCLMultilineParams` | | **Tab Navigation** | Jump between parameters with `` in insert mode | | **Workspace UDT Scanner** | Scans project for user-defined types (.udt files) | | **Workspace Global DB Scanner** | Scans project for global data blocks (.db files) | | **blink.cmp Integration** | Smart completion source for blink.cmp | | **Attribute Block Toggle** | Interactive expand/collapse of `{...}` blocks with `xa` | ## Installation The plugin auto-detects the mason-installed `tia-lsp` executable via `vim.fn.exepath("tia-lsp")` and falls back to `lua ` for manual installs. See [Mason installation](#mason-installation) below for the recommended path. ### LazyVim ```lua -- ~/.config/nvim/lua/plugins/tia-lsp.lua return { "lazar/tia-lsp.nvim", ft = "scl", lazy = true, dependencies = { "neovim/nvim-lspconfig", "nvim-treesitter/nvim-treesitter", "saghen/blink.cmp", }, config = function() require("tia_lsp").setup({ -- LSP options lsp = { on_attach = function(client, bufnr) -- custom keymaps end, }, cmp = true, -- Enable blink.cmp integration workspace_types = true, -- Enable workspace UDT scanning auto_prefix = true, -- Enable auto-# prefixing debug = false, }) end, } ``` ### Manual Setup ```lua -- In your init.lua or plugin file require("tia_lsp").setup({ server_path = "/path/to/tia-lsp/src/main.lua", -- only used if tia-lsp is not on PATH ts_parser_url = "https://your-gitea/tia-lsp.git", -- tree-sitter parser source cmp = true, workspace_types = true, auto_prefix = true, }) ``` ### Mason installation `tia-lsp` can be installed via [mason.nvim](https://github.com/mason-org/mason.nvim) using a custom registry. The setup below uses a `file:` registry sourced from a Gitea-hosted `tia-mason-registry` repo (cloned by lazy.nvim), and falls back to the official mason registry for any other packages. 1. Clone the registry repo as a lazy.nvim plugin: ```lua -- ~/.config/nvim/lua/plugins/tia-mason-registry.lua return { url = "https://your-gitea/tia-mason-registry.git", name = "tia-mason-registry", lazy = false, -- must be available before mason setup } ``` 2. Register the registry with mason: ```lua -- ~/.config/nvim/lua/plugins/mason.lua return { "mason-org/mason.nvim", opts = { registries = { "file:" .. vim.fn.expand("~/.local/share/nvim/lazy/tia-mason-registry"), "github:mason-org/mason-registry", }, }, } ``` 3. Install `yq` (one-time prerequisite for mason's `file:` registry): ``` :MasonInstall yq ``` 4. Install the server: ``` :MasonInstall tia-lsp ``` 5. Verify: open a `.scl` file and run `:LspInfo` — `tia_lsp` should be attached, and `vim.fn.exepath("tia-lsp")` should resolve to `~/.local/share/nvim/mason/bin/tia-lsp`. To upgrade: tag a new `vX.Y.Z` release on the `tia-lsp` Gitea repo, bump `source.id` in `tia-mason-registry/packages/tia-lsp/package.yaml`, push the registry repo, then `:MasonInstall tia-lsp` again. ## Configuration Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `server_path` | string | `~/dev/tia-lsp/src/main.lua` | Path to LSP server (only used if `tia-lsp` is not on PATH) | | `ts_parser_url` | string | `file://~/dev/tia-lsp` | URL passed to nvim-treesitter for the SCL parser source | | `lsp.on_attach` | function | `nil` | Custom LSP attach callback | | `lsp.formatting` | boolean | `false` | Enable format command | | `cmp` | boolean | `true` | Enable blink.cmp integration | | `workspace_types` | boolean | `true` | Enable workspace UDT scanning | | `auto_prefix` | boolean | `true` | Enable auto-# prefixing | | `project_root` | string | `nil` | Custom project root | | `library_paths` | table | `{}` | Additional library paths | | `debug` | boolean | `false` | Enable debug logging | ## 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 | | `:SCLFormat` | Format current SCL file | | `:SCLToggleAttrBlock` | Toggle attribute block under cursor | | `:SCLExpandAllAttrBlocks` | Expand all `{...}` blocks in buffer | | `:SCLCollapseAllAttrBlocks` | Collapse all matching attribute blocks | ## Keybindings | Key | Action | |-----|--------| | `K` | Hover (show variable info) | | `gd` | Go to Definition | | `s` | Document Symbols | | `w` | Workspace Symbols | | `mp` | Fill multiline FB/Function parameters (insert mode) | | `` | Jump to next parameter (insert mode) | | `xa` | Toggle attribute block under cursor | | `xae` | Expand all attribute blocks | | `xac` | Collapse all attribute blocks | | `fw` | Workspace Symbols (Telescope) | | `ca` | Code Actions | ## Attribute Block Toggle Interactive expand/collapse of SCL variable attribute blocks: ```scl -- Before: Verbose attributes statCounter{EXTERNALACCESSIBLE := 'false'; EXTERNALVISIBLE := 'false'} : Int; -- After: Collapsed statCounter{...} : Int; ``` **Usage:** - Place cursor inside any `{...}` block - Press `xa` to toggle expand/collapse - Use `xae` to expand all blocks in buffer - Use `xac` to collapse all blocks **Note:** Collapsed blocks are automatically expanded before saving to preserve full content on disk. Use `:SCLFormat` to format, then `:SCLCollapseAllAttrBlocks` to collapse while preserving the ability to expand later. ## External UDT Support ### Option 1: Create `.plc.json` ```json { "dataTypes": [ { "name": "equipmentData", "struct": [ {"name": "id", "dataTypeName": "INT"}, {"name": "status", "dataTypeName": "BOOL"}, {"name": "temperature", "dataTypeName": "REAL"} ] } ] } ``` ### Option 2: Auto-scan `data_types/` folder The LSP automatically scans for SCL files in `data_types/` folder and extracts TYPE definitions. ### Option 3: Generate `.plc.json` ```bash lua ~/dev/tia-lsp/src/plc_json.lua --generate ``` ## Global Data Block Support The plugin automatically scans for `.db` files in the workspace and provides auto-completion for global data blocks and their members. ### How it works 1. **Workspace Scanning**: When opening a `.scl` file, the plugin scans the project for `.db` files 2. **DB Parsing**: Parses `DATA_BLOCK` definitions and extracts members from `VAR` sections 3. **Auto-completion**: Provides smart completion for DB names and members ### Usage - Type a space or `#` to trigger completion - global DB names appear alongside local variables - Type `"` followed by a DB name to get member completions - Access nested members: `"MyDB".member.submember` ### Example ```scl // Access global DB members "HmiData".units "Parameter".machine.unit100.paramTimeStampApplied // Inside FB call parameter #instParameterManager(hmiInterface:="HmiData".units, ...) ``` ### Project Structure ``` tia-lsp/ ├── README.md # This file ├── package.json # NPM scripts (tree-sitter) ├── Makefile # Build commands ├── grammar.js # Tree-sitter grammar (SCL language) ├── tree-sitter.json # Parser config ├── queries/ # Tree-sitter queries │ └── scl/ # Per-language query dir (SCL) │ ├── highlights.scm # Syntax highlighting │ ├── indents.scm # Indentation │ ├── folds.scm # Code folding │ ├── locals.scm # Variable scoping │ └── tags.scm # Navigation tags ├── src/ # LSP server │ ├── main.lua # Main entry point (JSON-RPC over stdio) │ ├── parser.lua # SCL parser (will become parser_scl.lua + dispatch) │ ├── treesitter.lua # Tree-sitter integration │ ├── json.lua # JSON encoder/decoder │ ├── plc_json.lua # External UDT loading (language-agnostic) │ ├── diagnostics.lua # Linter diagnostics │ └── formatter.lua # Document formatter └── lua/ ├── tia_lsp/ # Neovim plugin │ ├── init.lua # Main plugin setup │ └── plugins/ │ └── scl.lua # LazyVim plugin spec (SCL filetype) └── tia/ # Language modules (shared across TIA languages) ├── auto_prefix.lua ├── attr_toggle.lua ├── blink_cmp_source.lua ├── builtin_instructions.lua ├── db_parser.lua ├── fb_parser.lua ├── multiline_params.lua ├── udt_parser.lua ├── variables.lua └── workspace_types.lua ``` ## Building Tree-sitter Parser ```bash # Generate parser from grammar.js npm run build # or: tree-sitter generate # Run tests npm test # or: tree-sitter test ``` ## SCL Code Example ```pascal FUNCTION_BLOCK "Em101Sequence" VAR x : INT; myVar : equipmentData; // User-defined type END_VAR IF x > 5 THEN myVar.id := 10; END_IF myVar.status := TRUE; myVar.temperature := 25.5; END_FUNCTION_BLOCK ``` ## Architecture The plugin uses a standalone LSP server (`src/main.lua`) that communicates via JSON-RPC over stdio. The server handles: - Text document synchronization - Diagnostics (linting) - Formatting - Completion, hover, go-to-definition - Semantic tokens The Neovim plugin (`lua/tia_lsp/init.lua`) manages: - LSP client lifecycle via `vim.lsp.start()` - FileType autocmd for lazy loading - blink.cmp integration - Workspace scanning for UDTs/DBs ## Requirements - **Neovim 0.11+** (uses `vim.lsp.start()`) - **nvim-treesitter** - Syntax highlighting - **blink.cmp** - Optional, for completion - **Lua 5.1+** - LSP server runtime