Variable labels use # prefix (#instTestTimer) but users type without # (instTestTimer). Without filterText, the client filters using the label and #instTestTimer doesn't match instTestTimer, so the variable never appears in autocomplete. Setting filterText to the bare variable name fixes this. Also removed aggressive fallback in else branch that treated any word typed as function call context even without a ( in the line.
tia-lsp - Language Server for Siemens TIA Portal
A standalone LSP (Language Server Protocol) server for Siemens TIA Portal languages. Currently supports SCL (Structured Control Language), with planned support for STL/AWL, LAD, and FBD.
The server speaks JSON-RPC over stdio and is editor-agnostic — usable from Neovim, VS Code, or any LSP client. For Neovim users, the companion plugin tia-lsp.nvim provides seamless integration (auto-prefix, blink.cmp source, workspace scanning, attribute toggle, etc.).
Features
LSP (Language Server Protocol)
| Feature | Description |
|---|---|
| Hover | Variable type and struct fields |
| Completion | # for variables, . for struct fields, ( for functions |
| Go to Definition | 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 |
| 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 |
Tree-sitter Grammar
- 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 (/* */)
Running the Server
# Start the LSP server (reads JSON-RPC from stdin, writes to stdout)
lua src/main.lua
# Run server tests
make test
# or: lua src/main.lua --test
Neovim + Mason (Recommended)
The recommended way is via mason.nvim using the
tia-mason-registry. See the
tia-lsp.nvim README for full step-by-step instructions.
Manual Setup
# 1. Clone this repository
git clone --depth 1 https://gitea.l-tech.rs/lazar/tia-lsp.git ~/tia-lsp
# 2. Compile the tree-sitter parser
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
# 3. Start the server (or configure your editor to run this)
lua ~/tia-lsp/src/main.lua
External UDT Support
Option 1: Create .plc.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
lua src/plc_json.lua --generate
Global Data Block Support
The server scans for .db files in the workspace and provides auto-completion for global data blocks and their members.
How it works
- Workspace Scanning: When opening a
.sclfile, the server scans the project for.dbfiles - DB Parsing: Parses
DATA_BLOCKdefinitions and extracts members fromVARsections - Auto-completion: Provides completion for DB names and members
Example
// Access global DB members
"HmiData".units
"Parameter".machine.unit100.paramTimeStampApplied
// Inside FB call parameter
#instParameterManager(hmiInterface:="HmiData".units, ...)
Building Tree-sitter Parser
# Generate parser from grammar.js
tree-sitter generate
# Run parser tests
tree-sitter test
# Parse a single SCL file
tree-sitter parse <file.scl>
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
│ ├── grammar.json # Generated tree-sitter grammar
│ ├── node-types.json # Generated node types
│ ├── parser.c # Generated tree-sitter parser
│ └── tree_sitter/ # Tree-sitter C runtime headers
├── grammar.js # Tree-sitter grammar (language name: 'scl')
├── tree-sitter.json # Parser config
├── package.json # NPM scripts (tree-sitter)
├── Makefile # Build commands
├── generate_plc_json.lua # .plc.json generator utility
└── test/ # Server tests
├── run_tests.lua
├── test_harness.lua
├── test_formatter.lua
└── test_plc_json.lua
Architecture
The server (src/main.lua) communicates via JSON-RPC over stdio and handles:
- Text document synchronization
- Diagnostics (linting)
- Formatting
- Completion, hover, go-to-definition
- Semantic tokens
- Inlay hints
- Workspace symbols
The Neovim plugin (tia-lsp.nvim) manages the LSP client lifecycle, editor features, and workspace scanning. See its README for details.
SCL Code Example
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
Requirements
- Lua 5.1+ — LSP server runtime
- tree-sitter CLI — For regenerating the parser from
grammar.js(only needed during development) - Node.js — For tree-sitter CLI (only needed during development)
Testing
# Run server tests
lua test/run_tests.lua
# Run individual test files
lua test/test_formatter.lua
lua test/test_plc_json.lua
# Run LSP server in test mode
lua src/main.lua --test
Tests use files from a reference project (override with SCL_REF_PROJECT=/path/to/ref lua test/run_tests.lua).