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.
359 lines
13 KiB
Markdown
359 lines
13 KiB
Markdown
# 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](#mason-installation) below for the recommended path.
|
|
|
|
### LazyVim
|
|
```lua
|
|
-- ~/.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
|
|
```lua
|
|
-- 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](https://github.com/mason-org/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:
|
|
```lua
|
|
-- ~/.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:
|
|
```lua
|
|
-- ~/.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 `:LspInfo` — `tia_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:
|
|
|
|
```scl
|
|
-- 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`
|
|
```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`
|
|
```bash
|
|
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
|
|
|
|
```scl
|
|
// 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
|
|
|
|
```bash
|
|
# Generate parser from grammar.js
|
|
npm run build
|
|
# or: tree-sitter generate
|
|
|
|
# Run tests
|
|
npm test
|
|
# or: tree-sitter test
|
|
```
|
|
|
|
## SCL Code Example
|
|
|
|
```pascal
|
|
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
|