The semantic tokens were causing 'Invalid line: out of range' errors when opening certain files. This appears to be related to the delta mode not being properly handled. Disabled semantic tokens for now to prevent the errors.
SCL Language Server - Unified Plugin for Siemens SCL
A comprehensive Neovim/LazyVim plugin providing Language Server Protocol (LSP), Linting, Formatting, and Syntax Highlighting for Siemens SCL (Structured Control Language) used in Siemens TIA Portal.
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 variable declaration |
| 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 |
| Indentation | Proper block indentation |
| Keyword Casing | Maintains SCL keyword casing |
Syntax Highlighting
- Full SCL syntax support via tree-sitter
- ORGANIZATION_BLOCK, FUNCTION_BLOCK, FUNCTION definitions
- Variable declarations (VAR_INPUT, VAR_OUTPUT, VAR_IN_OUT, VAR_TEMP, etc.)
- Control structures (IF/THEN/ELSE, CASE, FOR, WHILE, REPEAT)
- Code regions and comments
- All SCL data types and operators
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
LazyVim
-- ~/.config/nvim/lua/plugins/scl.lua
return {
"lazar/scl_lsp",
ft = "scl",
lazy = true,
dependencies = {
"neovim/nvim-lspconfig",
"nvim-treesitter/nvim-treesitter",
"saghen/blink.cmp",
},
config = function()
require("scl_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("scl_lsp").setup({
server_path = "/path/to/scl_lsp/src/main.lua",
cmp = true,
workspace_types = true,
auto_prefix = true,
})
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
server_path |
string | ~/dev/scl_lsp/src/main.lua |
Path to LSP server |
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 |
:LspSCLFormat |
Format current SCL file |
:SCLGeneratePlcJson |
Generate plc.data.json from data_types/ |
: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: The formatter has automatic collapsing disabled by default to preserve compatibility with the toggle feature. Format with :LspSCLFormat first, then use :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 /home/lazar/dev/scl_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
- Workspace Scanning: When opening a
.sclfile, the plugin scans the project for.dbfiles - DB Parsing: Parses
DATA_BLOCKdefinitions and extracts members fromVARsections - 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
scl_lsp/
├── README.md # This file
├── package.json # NPM scripts (tree-sitter)
├── Makefile # Build commands
├── grammar.js # Tree-sitter grammar
├── tree-sitter.json # Parser config
├── queries/ # Tree-sitter queries
│ ├── 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
│ ├── parser.lua # Regex-based parser
│ ├── treesitter.lua # Tree-sitter integration
│ ├── json.lua # JSON encoder/decoder
│ ├── plc_json.lua # External UDT loading
│ ├── diagnostics.lua # Linter diagnostics
│ └── formatter.lua # Document formatter
└── lua/
└── scl_lsp/
├── init.lua # Main plugin setup
├── plugins/
│ └── scl.lua # LazyVim plugin spec
└── scl/ # Syntax/completion modules
├── init.lua
├── auto_prefix.lua
├── blink_cmp_source.lua
├── db_parser.lua
├── fb_parser.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/scl_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