lazar 8c6a050efb refactor: rename scl_lsp → tia_lsp (project umbrella for future STL/LAD/FBD)
Rename the project from scl_lsp to tia_lsp to future-proof for additional
TIA Portal languages (STL/AWL, LAD, FBD). The 'scl' language name is kept
as-is for the SCL filetype and tree-sitter parser; future languages get
their own names (stl, lad, fbd).

Project-wide changes:
- lua/scl_lsp/ → lua/tia_lsp/  (plugin entry point)
- lua/scl/     → lua/tia/      (language modules, shared across TIA langs)
- require('scl.*')    → require('tia.*')
- require('scl_lsp')  → require('tia_lsp')
- LSP client name:    'scl_lsp' → 'tia_lsp'
- Diagnostic source:  'scl_lsp' → 'tia_lsp'  (src/diagnostics.lua)
- package.json name:  'scl-language-server' → 'tia-lsp'

lua/tia_lsp/init.lua:
- Add vim.fn.exepath('tia-lsp') detection so the plugin prefers the
  mason-installed executable and falls back to { 'lua', server_path }
  for manual installs.
- Add opts.ts_parser_url to configure the tree-sitter parser source.
- Remove hardcoded ~/dev/scl_lsp paths in favor of ~/dev/tia-lsp and
  the new ts_parser_url option.

Cleanup:
- Delete lua/tia/init.lua (dead code, unreferenced duplicate of tia_lsp).
- Delete scl_lsp.sh (replaced by the mason-installed bin/tia-lsp wrapper).
- Update README.md and AGENTS.md to reflect the two-repo split
  (tia-lsp server + tia-lsp.nvim plugin) and document Mason installation.

Unchanged (intentional):
- grammar.js, src/grammar.json, src/parser.c: tree-sitter language name
  remains 'scl' (it's the language, not the project).
- Filetype patterns ('scl', 'udt', 'db') in autocmds.
- :SCL* command names (per-filetype prefix; future :STL* etc.).

Tests pass with the same pre-existing failures as before the rename
(formatter VAR_INPUT test, udt_parser line 199 const-variable bug,
plc_json reference-project-missing). No new failures introduced.
2026-07-18 11:30:55 +02:00

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 <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
Attribute Block Toggle Interactive expand/collapse of {...} blocks with <Leader>xa

Installation

The plugin auto-detects the mason-installed tia-lsp executable via vim.fn.exepath("tia-lsp") and falls back to lua <server_path> for manual installs. See Mason installation below for the recommended path.

LazyVim

-- ~/.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

-- 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 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:
    -- ~/.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:
    -- ~/.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 :LspInfotia_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
<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>xa to toggle expand/collapse
  • Use <Leader>xae to expand all blocks in buffer
  • Use <Leader>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

{
  "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 ~/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

// 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

# Generate parser from grammar.js
npm run build
# or: tree-sitter generate

# Run tests
npm test
# or: tree-sitter test

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

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
S
Description
No description provided
Readme
177 KiB
Languages
Lua 96.7%
Tree-sitter Query 3.2%