chore: adjust for plugin-only scope after split

After splitting from the original scl_lsp repo, update for plugin-only scope:

- README.md: focused on Neovim plugin installation, configuration,
  commands, keybindings, and editor features
- AGENTS.md: plugin-only project structure and conventions
- test/run_tests.lua: run only plugin tests (udt, db, fb, integration)
- test/test_integration.lua: remove plc_json dependency (server module)
- queries/: move from queries/*.scm to queries/scl/*.scm (nvim-treesitter
  per-language convention)
- ftdetect/scl.vim: add filetype detection for .scl/.udt/.db files
This commit is contained in:
2026-07-18 11:46:18 +02:00
parent 8c6a050efb
commit 8255db7f64
10 changed files with 121 additions and 296 deletions
+102 -240
View File
@@ -1,56 +1,9 @@
# tia-lsp - Language Server for Siemens TIA Portal
# tia-lsp.nvim - Neovim Plugin 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).
A Neovim/LazyVim plugin that launches the [`tia-lsp`](https://gitea.l-tech.rs/lazar/tia-lsp) language server and adds editor-specific features for Siemens TIA Portal languages. Currently supports **SCL** (Structured Control Language), with planned support for **STL/AWL**, **LAD**, and **FBD**.
## 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) |
@@ -60,65 +13,24 @@ The project is split into two repos:
| **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` |
| **Tree-sitter Highlighting** | Syntax highlighting via tree-sitter queries |
## 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.
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.
### 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,
}
```
### LazyVim + Mason (Recommended)
### 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:
1. Add the mason registry as a lazy.nvim plugin:
```lua
-- ~/.config/nvim/lua/plugins/tia-mason-registry.lua
return {
url = "https://your-gitea/tia-mason-registry.git",
url = "https://gitea.l-tech.rs/lazar/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
@@ -132,17 +44,54 @@ require("tia_lsp").setup({
},
}
```
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.
3. Add the plugin:
```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 = {
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,
}
```
4. Install:
```
:MasonInstall yq " one-time prerequisite for file: registry
:MasonInstall tia-lsp " installs the LSP server
```
5. Verify: open a `.scl` file and run `:LspInfo` — `tia_lsp` should be attached.
### Manual Setup (without Mason)
```lua
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://gitea.l-tech.rs/lazar/tia-lsp.git",
cmp = true,
workspace_types = true,
auto_prefix = true,
})
```
## Configuration Options
@@ -207,152 +156,65 @@ statCounter{...} : Int;
- 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.
**Note:** Collapsed blocks are automatically expanded before saving to preserve full content on disk.
## External UDT Support
## Filetype Detection
### Option 1: Create `.plc.json`
```json
{
"dataTypes": [
{
"name": "equipmentData",
"struct": [
{"name": "id", "dataTypeName": "INT"},
{"name": "status", "dataTypeName": "BOOL"},
{"name": "temperature", "dataTypeName": "REAL"}
]
}
]
}
```
The plugin includes `ftdetect/scl.vim` which registers the following file extensions as `scl` filetype:
- `*.scl` — SCL source files
- `*.udt` — User-defined type files
- `*.db` — Data block files
### 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
## 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)
tia-lsp.nvim/
├── lua/
│ ├── tia_lsp/ # Plugin entry point
│ │ ├── init.lua # Main setup (vim.lsp.start, exepath detection)
│ │ └── plugins/
│ │ └── scl.lua # LazyVim plugin spec (SCL filetype)
│ └── tia/ # Language modules (shared across TIA languages)
├── auto_prefix.lua # Auto-# prefixing
│ ├── attr_toggle.lua # Attribute block expand/collapse
│ ├── blink_cmp_source.lua # blink.cmp completion source
│ ├── builtin_instructions.lua
│ ├── db_parser.lua # Global DB parser
│ ├── fb_parser.lua # Function Block parser
│ ├── multiline_params.lua # Multiline parameter filling
│ ├── udt_parser.lua # UDT parser
│ ├── variables.lua # Variable extraction
│ └── workspace_types.lua # Workspace scanning
├── queries/
│ └── scl/ # Tree-sitter queries (per-language)
│ ├── 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
├── ftdetect/
── scl.vim # Filetype detection
└── test/ # Plugin tests
├── run_tests.lua
├── test_harness.lua
├── test_udt.lua
├── test_db.lua
── test_fb.lua
└── test_integration.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
- **tia-lsp** — the LSP server (install via mason or manually)
- **nvim-treesitter** — Syntax highlighting
- **blink.cmp** — Optional, for completion
- **Lua 5.1+** — LSP server runtime
## Testing
```bash
lua test/run_tests.lua
```
Tests use files from a reference project (override with `SCL_REF_PROJECT=/path/to/ref lua test/run_tests.lua`).