lazar 8b400054c6 fix: . trigger prefix extraction broken when . not yet in document
When nvim-cmp sends completion before didChange commits the ., the
cursor col is at the word end, but the old code always scanned from
col-1, cutting off the variable's last character (instTestTime vs
instTestTimer). Now checks if the character at col is . or empty;
if so, scans from col-1; otherwise scans from col.
2026-07-22 17:55:52 +02:00
2026-02-14 15:05:12 +01:00
2026-02-14 15:05:12 +01:00
2026-02-14 15:05:12 +01:00

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

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

  1. Workspace Scanning: When opening a .scl file, the server scans the project for .db files
  2. DB Parsing: Parses DATA_BLOCK definitions and extracts members from VAR sections
  3. 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).

S
Description
No description provided
Readme
897 KiB
Languages
C 93.3%
Lua 5.7%
JavaScript 0.6%
C++ 0.4%