# tia-lsp.nvim - Neovim Plugin for Siemens TIA Portal A Neovim/LazyVim plugin that launches the [`tia-lsp`](https://gitea.l-tech.rs/lazar/tia-lsp) language server and adds editor-specific features for Siemens TIA Portal languages. Currently supports **SCL** (Structured Control Language), with planned support for **STL/AWL**, **LAD**, and **FBD**. ## 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 — DB members, FB parameters, built-in types (IEC_TIMER IN/PT/Q/ET), nested UDT/array-of-UDT path resolution | | **Attribute Block Toggle** | Interactive expand/collapse of `{...}` blocks with `xa` | | **Tree-sitter Highlighting** | Syntax highlighting via tree-sitter queries | ## Architecture This plugin is part of a three-repo ecosystem: ``` tia-lsp.nvim ← Neovim plugin (you are here) — launches the LSP client, │ adds editor features (auto-# prefix, blink.cmp source, │ attribute toggle, workspace scanning, ...) ▼ tia-lsp ← Standalone LSP server — reads JSON-RPC over stdio, │ provides hover, completion, diagnostics, formatting │ Requires: lua src/main.lua + compiled parser.so ▲ tia-mason-registry ← mason.nvim recipe — tells mason how to download tia-lsp, compile parser.so, and create the tia-lsp executable ``` - **tia-lsp** ([repo](https://gitea.l-tech.rs/lazar/tia-lsp)) — the actual language server, editor-agnostic - **tia-mason-registry** ([repo](https://gitea.l-tech.rs/lazar/tia-mason-registry)) — a single `package.yaml` for mason - **tia-lsp.nvim** (this repo) — the Neovim-specific integration ## Installation The plugin auto-detects the mason-installed `tia-lsp` executable via `vim.fn.exepath("tia-lsp")`. If not found, it falls back to running `lua ` (configurable). ### LazyVim + Mason (Recommended) Mason downloads and builds the `tia-lsp` server; this plugin configures the LSP client. **Step 1: Add the mason registry as a lazy.nvim plugin** mason needs a local copy of the registry to read the install recipe. Adding it as a lazy.nvim plugin gets it cloned automatically: ```lua -- ~/.config/nvim/lua/plugins/tia-mason-registry.lua return { url = "https://gitea.l-tech.rs/lazar/tia-mason-registry.git", name = "tia-mason-registry", } ``` **Step 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", }, }, } ``` > **Why this works:** lazy.nvim clones `tia-mason-registry` to `~/.local/share/nvim/lazy/tia-mason-registry`. The `file:` registry path points mason to that directory, where it finds `packages/tia-lsp/package.yaml`. Mason needs [`yq`](https://github.com/mikefarah/yq) to parse YAML custom registries — install it once: `:MasonInstall yq` **Step 3: Install the LSP server** ``` :MasonInstall tia-lsp ``` This clones `tia-lsp` from the source repo, compiles `parser.so` (tree-sitter), and places the `tia-lsp` executable on PATH. **Step 4: Add this plugin** ```lua -- ~/.config/nvim/lua/plugins/tia-lsp.lua return { "https://gitea.l-tech.rs/lazar/tia-lsp.nvim", ft = "scl", lazy = true, dependencies = { "neovim/nvim-lspconfig", "nvim-treesitter/nvim-treesitter", "saghen/blink.cmp", -- optional, for completion }, opts = { lsp = { -- on_attach = function(client, bufnr) ... end, }, cmp = true, -- enable blink.cmp integration workspace_types = true, -- scan project for UDTs and DBs auto_prefix = true, -- auto-# prefix for local variables }, } ``` **Step 5: Configure blink.cmp defaults** The `scl` completion source must be listed in blink.cmp's `sources.default` with the corresponding provider definition. Create or update your blink.cmp config: ```lua -- ~/.config/nvim/lua/plugins/blink.lua (or extend your existing blink.cmp spec) return { "saghen/blink.cmp", optional = true, opts = { sources = { default = { "lsp", "path", "snippets", "buffer", "scl" }, providers = { scl = { name = "scl", module = "tia.blink_cmp_source", score_offset = 100, }, }, }, }, } ``` > **Why this is needed:** blink.cmp only queries sources listed in `sources.default`. Without `"scl"` there, the DB member and built-in type completions (e.g., `"MyDB".myField` or `#myTimer.IN`) will not appear. **Step 6: Verify** Open a `.scl` file and run `:LspInfo`. `tia_lsp` should be attached. Run `:checkhealth` for detailed diagnostics. --- ### Manual Setup (without Mason) Clone and build the server manually, then point the plugin at it. ```bash # 1. Clone the LSP server git clone --depth 1 https://gitea.l-tech.rs/lazar/tia-lsp.git ~/tia-lsp # 2. Compile the tree-sitter parser (required for parsing SCL) cc -fPIC -I ~/tia-lsp/src/tree_sitter -c ~/tia-lsp/src/parser.c -o ~/tia-lsp/parser.o cc -shared ~/tia-lsp/parser.o -o ~/tia-lsp/parser.so ``` Then configure the plugin. The plugin first looks for `tia-lsp` on PATH; if not found, it falls back to running `lua `. The tree-sitter parser is loaded from `parser.so` adjacent to the server. ```lua require("tia_lsp").setup({ server_path = "~/tia-lsp/src/main.lua", -- fallback if tia-lsp not on PATH ts_parser_url = "https://gitea.l-tech.rs/lazar/tia-lsp.git", cmp = true, workspace_types = true, auto_prefix = true, }) ``` ## Configuration Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `server_path` | string | `~/dev/tia-lsp/src/main.lua` | Path to LSP server (fallback when `tia-lsp` not on PATH; change for manual installs) | | `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. ## Filetype Detection The plugin includes `ftdetect/scl.vim` which registers the following file extensions as `scl` filetype: - `*.scl` — SCL source files - `*.udt` — User-defined type files - `*.db` — Data block files ## Project Structure ``` tia-lsp.nvim/ ├── lua/ │ ├── tia_lsp/ # Plugin entry point │ │ ├── init.lua # Main setup (vim.lsp.start, exepath detection) │ │ └── plugins/ │ │ └── scl.lua # LazyVim plugin spec (SCL filetype) │ └── tia/ # Language modules (shared across TIA languages) │ ├── auto_prefix.lua # Auto-# prefixing │ ├── attr_toggle.lua # Attribute block expand/collapse │ ├── blink_cmp_source.lua # blink.cmp completion source │ ├── builtin_instructions.lua │ ├── db_parser.lua # Global DB parser │ ├── fb_parser.lua # Function Block parser │ ├── multiline_params.lua # Multiline parameter filling │ ├── udt_parser.lua # UDT parser │ ├── variables.lua # Variable extraction │ └── workspace_types.lua # Workspace scanning ├── queries/ │ └── scl/ # Tree-sitter queries (per-language) │ ├── highlights.scm # Syntax highlighting │ ├── indents.scm # Indentation │ ├── folds.scm # Code folding │ ├── locals.scm # Variable scoping │ └── tags.scm # Navigation tags ├── ftdetect/ │ └── scl.vim # Filetype detection └── test/ # Plugin tests ├── run_tests.lua ├── test_harness.lua ├── test_udt.lua ├── test_db.lua ├── test_fb.lua └── test_integration.lua ``` ## Requirements - **Neovim 0.11+** (uses `vim.lsp.start()`) - **tia-lsp** — the LSP server (install via mason or manually) - **nvim-treesitter** — Syntax highlighting - **blink.cmp** — Optional, for completion - **Lua 5.1+** — LSP server runtime ## Troubleshooting | Symptom | Likely Cause | Fix | |---------|-------------|-----| | `:MasonInstall tia-lsp` fails with "unknown package" | Custom registry not registered with mason | Check the `registries` list in your mason config includes the `file:` path to the cloned registry | | `:MasonInstall tia-lsp` fails with "yq: command not found" | Mason needs `yq` to parse the YAML registry | Run `:MasonInstall yq` to install it, then retry `:MasonInstall tia-lsp` | | `tia_lsp` not attached after opening `.scl` | Server not on PATH or `server_path` points to wrong location | Run `:LspInfo` to see what server is configured. Check `vim.fn.exepath("tia-lsp")` returns a path | | LSP starts but no syntax highlighting | Tree-sitter parser (`parser.so`) not found | mason install generates it automatically. For manual setups, run the `cc` commands in the Manual Setup section | | `parser.so` load error at startup | Compiled for wrong architecture or Lua version | Recompile: clean the old `.so` and run the `cc` build commands again | | LSP starts but no completions/hover | Server can't find its modules | Mason installs to a managed directory. If using manual setup, ensure `src/` contents are present next to `main.lua` | ## Testing ```bash lua test/run_tests.lua ``` Tests use files from a reference project (override with `SCL_REF_PROJECT=/path/to/ref lua test/run_tests.lua`).