- Fix path extraction pattern (missing closing quote, extra capture group) - Add Array[..] of "UdtName" detection for nested member resolution - Remove vim.schedule_wrap from DB member callback (causes blink.cmp timeout) - Add built-in instruction parameter fallback (IEC_TIMER IN/PT/Q/ET) - Ensure DB cache is populated on demand in completion source - Make try_register() succeed without cmp.add_source_provider API - docs: add blink.cmp source config steps to installation guide
tia-lsp.nvim - Neovim Plugin for Siemens TIA Portal
A Neovim/LazyVim plugin that launches the 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 <Space>mp or :SCLMultilineParams |
| Tab Navigation | Jump between parameters with <Tab> 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 <Leader>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) — the actual language server, editor-agnostic
- tia-mason-registry (repo) — a single
package.yamlfor 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 <server_path> (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:
-- ~/.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
-- ~/.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-registryto~/.local/share/nvim/lazy/tia-mason-registry. Thefile:registry path points mason to that directory, where it findspackages/tia-lsp/package.yaml. Mason needsyqto 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
-- ~/.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:
-- ~/.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".myFieldor#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.
# 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 <server_path>. The tree-sitter parser is loaded from parser.so adjacent to the server.
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 |
<leader>s |
Document Symbols |
<leader>w |
Workspace Symbols |
<Space>mp |
Fill multiline FB/Function parameters (insert mode) |
<Tab> |
Jump to next parameter (insert mode) |
<Leader>xa |
Toggle attribute block under cursor |
<Leader>xae |
Expand all attribute blocks |
<Leader>xac |
Collapse all attribute blocks |
<leader>fw |
Workspace Symbols (Telescope) |
<leader>ca |
Code Actions |
Attribute Block Toggle
Interactive expand/collapse of SCL variable attribute blocks:
-- Before: Verbose attributes
statCounter{EXTERNALACCESSIBLE := 'false'; EXTERNALVISIBLE := 'false'} : Int;
-- After: Collapsed
statCounter{...} : Int;
Usage:
- Place cursor inside any
{...}block - Press
<Leader>xato toggle expand/collapse - Use
<Leader>xaeto expand all blocks in buffer - Use
<Leader>xacto 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
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).